|
root / docs / superpowers / plans / 2026-05-12-zyginit-visual-pass.md
2026-05-12-zyginit-visual-pass.md markdown 1968 lines 62.2 KB

zyginit Visual Pass 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: Implement the unified visual identity defined in docs/superpowers/specs/2026-05-12-zyginit-visual-pass-design.md: boot rolling tape, progress bar, spinner, final cards, shutdown mirror, and zygctl status banner.

Architecture: Single new module src/ui.reef owns all rendering — palette, glyphs, sigil, TTY/TERM detection, ANSI sequences, rolling tape state, progress bar, spinner. Existing println lifecycle sites in main.reef and supervisor.reef route through it. zygctl client detects its own TTY and asks the daemon for STATUS PLAIN when piped. A --ui-demo <scenario> flag on zyginit drives snapshot-based tests against canned event sequences (no daemon needed).

Tech Stack: Reef 0.4+; FFI via extern "C" to a new zyginit_isatty helper in src/helpers.c; SGR / VT100 sequences for rich rendering; existing integration shell harness for end-to-end tests; Mercurial for VCS.


File Structure

Create:

  • src/ui.reef — rendering module (all UI state and procedures)
  • tests/integration/snapshots/ — golden output files for snapshot tests
  • tests/integration/ui_tests.sh — shell-based snapshot test runner

Modify:

  • src/helpers.c — add zyginit_isatty(int fd) (returns 1/0)
  • src/main.reef — wire ui_init, ui_boot_start, ui_tier_start/done, ui_boot_complete, ui_shutdown_start/complete, ui_tick; add --ui-demo <scenario> flag dispatch
  • src/supervisor.reef — replace ~8 lifecycle println sites with ui_event_* calls
  • src/socket.reefcmd_status / cmd_list delegate to ui_render_*; parse PLAIN modifier
  • tools/zygctl/src/main.reef — pre-flight detect_rich_mode; send STATUS or STATUS PLAIN
  • tests/integration/run_tests.shexport ZYGINIT_NO_UI=1 for diff stability
  • reef.toml, tools/zygctl/reef.toml, tools/sysv-wrapper/Makefile, src/version.reef, tools/zygctl/src/version.reef — version bump 0.1.2 → 0.1.3 via the existing scripts/bump-version.sh

Branching: all work on a fresh Mercurial bookmark visual-pass off rev 122.


Task 1: ui.reef skeleton + isatty C helper + --ui-demo flag

Files:

  • Create: src/ui.reef

  • Modify: src/helpers.c (append zyginit_isatty)

  • Modify: src/main.reef (argv parsing for --ui-demo)

  • Test: tests/integration/ui_tests.sh (new)

  • Step 1: Write the failing snapshot test for --ui-demo skeleton

Create tests/integration/ui_tests.sh:

#!/bin/sh
# Snapshot tests for the ui.reef renderer.
# Each scenario invokes `zyginit --ui-demo <name>` and diffs against a golden file.
set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
ZYGINIT="$PROJECT_DIR/build/zyginit"
SNAP_DIR="$SCRIPT_DIR/snapshots"

mkdir -p "$SNAP_DIR"
PASS=0
FAIL=0

check_scenario() {
    name=$1
    env_prefix=$2
    expected="$SNAP_DIR/$name.txt"
    got=$(mktemp)
    eval "$env_prefix $ZYGINIT --ui-demo $name" > "$got" 2>&1
    if [ ! -f "$expected" ]; then
        cp "$got" "$expected"
        echo "  [created]  $name"
        PASS=$((PASS+1))
        rm -f "$got"
        return
    fi
    if diff -u "$expected" "$got" > /dev/null; then
        echo "  [pass]     $name"
        PASS=$((PASS+1))
    else
        echo "  [FAIL]     $name"
        diff -u "$expected" "$got" || true
        FAIL=$((FAIL+1))
    fi
    rm -f "$got"
}

echo "ui snapshot tests"
check_scenario skeleton "ZYGINIT_NO_UI=1"

echo
echo "passed: $PASS  failed: $FAIL"
[ "$FAIL" -eq 0 ]

Then make it executable: chmod +x tests/integration/ui_tests.sh

  • Step 2: Run the test to confirm it fails

Run: ./tests/integration/ui_tests.sh
Expected: FAIL — build/zyginit either doesn't exist or doesn't recognize --ui-demo. Either is the failing condition we want.

  • Step 3: Add the zyginit_isatty C helper

In src/helpers.c, append after the existing helpers (before the final closing line):

/* zyginit_isatty: thin wrapper around isatty(3) so Reef FFI sees an int
 * return. Returns 1 if fd is a terminal, 0 otherwise. */
#include <unistd.h>
int zyginit_isatty(int fd) {
    return isatty(fd) ? 1 : 0;
}
  • Step 4: Create src/ui.reef skeleton

Create src/ui.reef:

/* SRCHEADER goes here — copy from project root SRCHEADER.txt, fill in:
   Project: zyginit, Filename: ui.reef,
   Description: Unified visual rendering: palette, glyphs, sigil,
                rolling tape, progress bar, spinner, final cards. */

module ui

import core.str as str
import sys.env as env

// Render modes. Decided once at startup by ui_init().
fn MODE_PLAIN(): int        return 0   end MODE_PLAIN
fn MODE_RICH_ASCII(): int   return 1   end MODE_RICH_ASCII
fn MODE_RICH_16(): int      return 2   end MODE_RICH_16
fn MODE_RICH_TRUE(): int    return 3   end MODE_RICH_TRUE

mut g_mode: int = 0
mut g_failed: bool = false   // set if write() to /dev/console errors

extern "C" fn zyginit_isatty(fd: int): int

proc ui_init(rich_mode: int)
    g_mode = rich_mode
    g_failed = false
end ui_init

fn ui_mode(): int
    return g_mode
end ui_mode

// Scenario dispatcher for --ui-demo. Called from main.reef.
// Returns 0 on success, 1 if the scenario name is unknown.
fn ui_demo(scenario: string): int
    if scenario == "skeleton"
        println("ui.reef skeleton ok, mode=" + int_to_str(g_mode))
        return 0
    end if
    println("ui.reef: unknown demo scenario: " + scenario)
    return 1
end ui_demo

end module
  • Step 5: Wire --ui-demo into src/main.reef

Find the argv parsing block near the top of main() in src/main.reef. Before the existing --version check, add:

// --ui-demo <scenario>: run a canned UI scenario for snapshot testing.
if args.has_flag(argv, "--ui-demo")
    let scenario = args.get_flag_value(argv, "--ui-demo")
    ui.ui_init(ui.MODE_PLAIN())
    process.exit(ui.ui_demo(scenario))
end if

And add import ui to the imports near the top of src/main.reef.

  • Step 6: Build and re-run the test
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/ui_tests.sh

Expected: snapshot is auto-created on first run. Output line: ui.reef skeleton ok, mode=0.

  • Step 7: Commit
hg add src/ui.reef tests/integration/ui_tests.sh tests/integration/snapshots/skeleton.txt
hg commit -m "ui: skeleton + --ui-demo flag + isatty C helper"

Task 2: detect_rich_mode and plain-mode line renderer

Files:

  • Modify: src/ui.reef

  • Modify: tests/integration/ui_tests.sh

  • Step 1: Add failing snapshot tests for plain-mode scenarios

In tests/integration/ui_tests.sh, add after the skeleton check:

check_scenario plain_boot "ZYGINIT_NO_UI=1 TERM=xterm-256color"
check_scenario detect_no_color "NO_COLOR=1 TERM=xterm-256color"
check_scenario detect_dumb "TERM=dumb"
check_scenario detect_sun "TERM=sun"
check_scenario detect_sun_color "TERM=sun-color"
  • Step 2: Run tests to confirm failure

Run: ./tests/integration/ui_tests.sh
Expected: FAILs — scenarios are unknown.

  • Step 3: Implement detect_rich_mode and plain-mode emitter in src/ui.reef

Add to src/ui.reef (after the mode constants, before ui_init):

fn detect_rich_mode(): int
    if env.has_env("ZYGINIT_NO_UI")     return MODE_PLAIN()     end if
    if env.has_env("NO_COLOR")          return MODE_PLAIN()     end if
    // PID check: non-PID-1 zyginit instances always emit plain.
    // sys.process.getpid is imported at top of file.
    if process.getpid() != 1
        // Allow override for unit tests via --ui-demo: caller calls ui_init() directly.
        // detect_rich_mode is only invoked from boot path in main.reef.
        return MODE_PLAIN()
    end if
    if zyginit_isatty(1) == 0           return MODE_PLAIN()     end if
    let term = env.get_env_or("TERM", "")
    if str.length(term) == 0            return MODE_PLAIN()     end if
    if term == "dumb"                   return MODE_PLAIN()     end if
    if env.has_env("ZYGINIT_ASCII")     return MODE_RICH_ASCII() end if
    if term == "sun-color"              return MODE_RICH_TRUE() end if
    if str.index_of(term, "256color") >= 0
        return MODE_RICH_TRUE()
    end if
    return MODE_RICH_16()
end detect_rich_mode

Add import sys.process as process at the top of src/ui.reef.

Then add a plain-mode emitter:

// Plain-mode line: "  <elapsed-s>  <level>  <event>     <name>  [k=v ...]"
// Used for boot/shutdown tape entries in MODE_PLAIN and as fallback.
fn fmt_plain_event(elapsed_ms: int, level: string, event: string,
                   name: string, kv: string): string
    let secs = int_to_str(elapsed_ms / 1000)
    let frac = int_to_str((elapsed_ms mod 1000) / 10)   // 2-digit decimal
    let pad = ""
    if str.length(frac) < 2
        pad = "0"
    end if
    mut line = str.concat("  ", secs)
    line = str.concat(line, ".")
    line = str.concat(line, pad)
    line = str.concat(line, frac)
    line = str.concat(line, "  ")
    line = str.concat(line, level)
    line = str.concat(line, "  ")
    line = str.concat(line, event)
    line = str.concat(line, "  ")
    line = str.concat(line, name)
    if str.length(kv) > 0
        line = str.concat(line, "  ")
        line = str.concat(line, kv)
    end if
    return line
end fmt_plain_event
  • Step 4: Add demo scenarios to ui_demo

Replace the ui_demo body in src/ui.reef:

fn ui_demo(scenario: string): int
    if scenario == "skeleton"
        println("ui.reef skeleton ok, mode=" + int_to_str(g_mode))
        return 0
    end if
    if scenario == "plain_boot"
        println(fmt_plain_event(40, "info", "started", "root-fs", ""))
        println(fmt_plain_event(120, "info", "started", "crypto", "dur_ms=80"))
        println(fmt_plain_event(310, "info", "started", "devfs", "dur_ms=190"))
        println(fmt_plain_event(5020, "err",  "failed",  "network", "exit=1 dur_ms=4023"))
        return 0
    end if
    if str.index_of(scenario, "detect_") == 0
        // Override g_mode by calling detect_rich_mode under the test env.
        g_mode = detect_rich_mode()
        println("mode=" + int_to_str(g_mode))
        return 0
    end if
    println("ui.reef: unknown demo scenario: " + scenario)
    return 1
end ui_demo
  • Step 5: Build and re-run tests
clang -c src/helpers.c -o build/helpers.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/ui_tests.sh

Expected snapshots get auto-created. Verify their content by hand:

cat tests/integration/snapshots/plain_boot.txt

Should show 4 lines, last one being the failed network event. Check the detect snapshots match expectations:

  • detect_no_color.txt: mode=0
  • detect_dumb.txt: mode=0
  • detect_sun.txt: mode=2 (RICH_16)
  • detect_sun_color.txt: mode=3 (RICH_TRUE)

If any are wrong, fix detect_rich_mode and re-run.

  • Step 6: Commit
hg add tests/integration/snapshots/*.txt
hg commit -m "ui: detect_rich_mode + plain-mode line formatter + snapshot tests"

Task 3: Rich primitives — palette, glyphs, sigil

Files:

  • Modify: src/ui.reef

  • Modify: tests/integration/ui_tests.sh

  • Step 1: Add failing snapshots for color and glyph helpers

In tests/integration/ui_tests.sh:

check_scenario palette_true "TERM=xterm-256color"
check_scenario palette_16   "TERM=sun"
check_scenario glyphs_unicode "TERM=xterm-256color"
check_scenario glyphs_ascii   "TERM=xterm-256color ZYGINIT_ASCII=1"
check_scenario sigil_rich   "TERM=sun-color"
  • Step 2: Run tests — confirm failure

Run: ./tests/integration/ui_tests.sh
Expected: 5 new FAILs.

  • Step 3: Add palette + glyph + sigil functions to src/ui.reef
// SGR escape sequences. CSI is "\e[", reset is "\e[0m".
fn esc_reset(): string return "\x1b[0m" end esc_reset

// 16-color foreground codes.
fn sgr_steel(): string  return "\x1b[37m"   end sgr_steel    // white
fn sgr_teal(): string   return "\x1b[36m"   end sgr_teal     // cyan
fn sgr_ok(): string     return "\x1b[32m"   end sgr_ok       // green
fn sgr_warn(): string   return "\x1b[33m"   end sgr_warn     // yellow
fn sgr_fail(): string   return "\x1b[31m"   end sgr_fail     // red
fn sgr_mute(): string   return "\x1b[2m"    end sgr_mute     // dim

// Truecolor foreground codes.
fn sgr_steel_true(): string return "\x1b[38;2;110;123;139m"  end sgr_steel_true
fn sgr_teal_true(): string  return "\x1b[38;2;0;106;111m"    end sgr_teal_true
fn sgr_ok_true(): string    return "\x1b[38;2;46;139;87m"    end sgr_ok_true
fn sgr_warn_true(): string  return "\x1b[38;2;255;191;0m"    end sgr_warn_true
fn sgr_fail_true(): string  return "\x1b[38;2;200;32;31m"    end sgr_fail_true

// Wrap text in the given palette token, honoring g_mode.
fn paint(token: string, text: string): string
    if g_mode == MODE_PLAIN()  return text  end if
    mut on = ""
    if g_mode == MODE_RICH_TRUE()
        if token == "frame"  on = sgr_steel_true()
        else if token == "accent" on = sgr_teal_true()
        else if token == "ok" on = sgr_ok_true()
        else if token == "warn" on = sgr_warn_true()
        else if token == "fail" on = sgr_fail_true()
        else if token == "mute" on = sgr_mute()
        end if
    else
        if token == "frame"  on = sgr_steel()
        else if token == "accent" on = sgr_teal()
        else if token == "ok" on = sgr_ok()
        else if token == "warn" on = sgr_warn()
        else if token == "fail" on = sgr_fail()
        else if token == "mute" on = sgr_mute()
        end if
    end if
    return str.concat(str.concat(on, text), esc_reset())
end paint

// Glyphs. Selected by g_mode.
fn glyph_ok(): string
    if g_mode == MODE_RICH_ASCII() return "#" end if
    if g_mode == MODE_PLAIN()      return "ok" end if
    return "●"
end glyph_ok

fn glyph_fail(): string
    if g_mode == MODE_RICH_ASCII() return "X" end if
    if g_mode == MODE_PLAIN()      return "fail" end if
    return "✕"
end glyph_fail

fn glyph_pending(): string
    if g_mode == MODE_RICH_ASCII() return "." end if
    if g_mode == MODE_PLAIN()      return "pending" end if
    return "·"
end glyph_pending

// Static starting glyph used when not animating (rare, e.g. snapshots).
fn glyph_starting(): string
    if g_mode == MODE_RICH_ASCII() return "o" end if
    if g_mode == MODE_PLAIN()      return "starting" end if
    return "◉"
end glyph_starting

// Sigil. Always "[Z]", colored c.accent in rich modes.
fn sigil(): string
    return paint("accent", "[Z]")
end sigil
  • Step 4: Add demo scenarios for the primitives

In ui_demo, add before the final unknown line:

    if scenario == "palette_true" or scenario == "palette_16"
        g_mode = detect_rich_mode()
        println(paint("frame",  "frame text"))
        println(paint("accent", "accent text"))
        println(paint("ok",     "ok text"))
        println(paint("warn",   "warn text"))
        println(paint("fail",   "fail text"))
        println(paint("mute",   "mute text"))
        return 0
    end if
    if scenario == "glyphs_unicode" or scenario == "glyphs_ascii"
        g_mode = detect_rich_mode()
        println(glyph_ok() + " " + glyph_starting() + " " +
                glyph_fail() + " " + glyph_pending())
        return 0
    end if
    if scenario == "sigil_rich"
        g_mode = detect_rich_mode()
        println(sigil() + " zyginit 0.1.3")
        return 0
    end if
  • Step 5: Build and verify snapshots
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/ui_tests.sh

By-hand sanity:

  • palette_true.txt contains \x1b[38;2; truecolor escapes.

  • palette_16.txt contains \x1b[37m etc — no truecolor.

  • glyphs_unicode.txt: ● ◉ ✕ ·

  • glyphs_ascii.txt: # o X .

  • sigil_rich.txt starts with \x1b[38;2;0;106;111m[Z]\x1b[0m.

  • Step 6: Commit

hg add tests/integration/snapshots/*.txt
hg commit -m "ui: palette + glyph + sigil primitives with mode-aware fallback"

Task 4: Boot header + rolling tape

Files:

  • Modify: src/ui.reef

  • Modify: tests/integration/ui_tests.sh

  • Step 1: Failing snapshot for boot tape scenario

In tests/integration/ui_tests.sh:

check_scenario boot_tape_rich  "TERM=xterm-256color"
check_scenario boot_tape_ascii "TERM=xterm ZYGINIT_ASCII=1"
check_scenario boot_tape_plain "ZYGINIT_NO_UI=1"

Run: ./tests/integration/ui_tests.sh → FAIL.

  • Step 2: Implement tape state + header + emit_started/failed

Add to src/ui.reef:

// Layout constants.
fn TAPE_HEIGHT(): int return 12 end TAPE_HEIGHT
fn LINE_WIDTH(): int  return 78 end LINE_WIDTH

// Boot/shutdown phase tracker for the header.
mut g_phase: string = ""        // "boot" | "shutdown"
mut g_runlevel: string = ""
mut g_num_svc: int = 0
mut g_num_tiers: int = 0
mut g_cur_tier: int = 0
mut g_done: int = 0
mut g_failed_count: int = 0
mut g_failed_first: string = ""
mut g_failed_first_exit: int = 0
mut g_boot_start_ms: int = 0    // monotonic ms at boot start

// Rolling tape: ring buffer of TAPE_HEIGHT lines.
type TapeLine = struct
    elapsed_ms: int
    glyph: string
    name: string
    note: string
end TapeLine

mut g_tape: [TapeLine] = new [TapeLine](12)
mut g_tape_head: int = 0
mut g_tape_count: int = 0

// time_now_ms: monotonic milliseconds. Wraps sys.time.time_now_ms when
// available; for the demo path we read a static counter.
extern "C" fn zyginit_monotonic_ms(): int

proc tape_push(elapsed_ms: int, glyph: string, name: string, note: string)
    let slot = g_tape_head mod TAPE_HEIGHT()
    g_tape[slot] = TapeLine{ elapsed_ms: elapsed_ms, glyph: glyph,
                             name: name, note: note }
    g_tape_head = g_tape_head + 1
    if g_tape_count < TAPE_HEIGHT()
        g_tape_count = g_tape_count + 1
    end if
end tape_push

// Format a tape row (mode-aware coloring on the glyph).
fn fmt_tape_row(line: TapeLine): string
    let secs = line.elapsed_ms / 1000
    let frac = (line.elapsed_ms mod 1000) / 10
    mut sf = int_to_str(frac)
    if str.length(sf) < 2  sf = str.concat("0", sf)  end if
    let elapsed = int_to_str(secs) + "." + sf
    // Pad elapsed to 6 cols right-aligned.
    mut pad = ""
    let pad_n = 6 - str.length(elapsed)
    mut i = 0
    while i < pad_n
        pad = str.concat(pad, " ")
        i = i + 1
    end while
    mut row = pad + elapsed + "  " + line.glyph + "  " + line.name
    if str.length(line.note) > 0
        row = str.concat(row, "  ")
        row = str.concat(row, paint("mute", "(" + line.note + ")"))
    end if
    return row
end fmt_tape_row

// Header banner + summary + failed-line + divider. 4 rows.
fn fmt_header(elapsed_ms: int): string
    let secs = elapsed_ms / 1000
    let frac = (elapsed_ms mod 1000) / 100
    let elapsed = int_to_str(secs) + "." + int_to_str(frac) + "s"
    let phase_label = g_phase + " · " + g_runlevel
    mut out = "  " + sigil() + " zyginit 0.1.3 · " + paint("frame", phase_label)
    out = str.concat(out, "  ")
    // pad to ~LINE_WIDTH then append elapsed
    out = str.concat(out, paint("accent", "elapsed " + elapsed))
    out = str.concat(out, "\n")
    out = str.concat(out, "  ")
    out = str.concat(out, paint("frame",
        int_to_str(g_num_svc) + " services · tier " + int_to_str(g_cur_tier)
        + " · " + int_to_str(g_done) + " done · "
        + int_to_str(g_failed_count) + " failed"))
    out = str.concat(out, "\n  ")
    if g_failed_count == 0
        out = str.concat(out, paint("mute", "failed: none"))
    else if g_failed_count == 1
        out = str.concat(out, paint("fail",
            "failed: " + g_failed_first + "  exit=" + int_to_str(g_failed_first_exit)))
    else
        out = str.concat(out, paint("fail",
            "failed: " + int_to_str(g_failed_count) + " services"))
    end if
    out = str.concat(out, "\n  ")
    mut div = ""
    mut k = 0
    while k < 62
        div = str.concat(div, "─")
        k = k + 1
    end while
    out = str.concat(out, paint("accent", div))
    return out
end fmt_header

// Render the entire screen for the current state.
fn render_boot_screen(elapsed_ms: int): string
    if g_mode == MODE_PLAIN()
        // Plain mode: no fixed regions; tape rows printed live as events fire.
        return ""
    end if
    mut out = fmt_header(elapsed_ms) + "\n"
    let start = g_tape_head - g_tape_count
    mut i = 0
    while i < TAPE_HEIGHT()
        if i < g_tape_count
            let slot = (start + i) mod TAPE_HEIGHT()
            out = str.concat(out, fmt_tape_row(g_tape[slot]))
        end if
        out = str.concat(out, "\n")
        i = i + 1
    end while
    return out
end render_boot_screen

// Lifecycle entry points.
proc ui_boot_start(num_svc: int, num_tiers: int, runlevel: string)
    g_phase = "boot"
    g_runlevel = runlevel
    g_num_svc = num_svc
    g_num_tiers = num_tiers
    g_cur_tier = 0
    g_done = 0
    g_failed_count = 0
    g_failed_first = ""
    g_failed_first_exit = 0
    g_tape_head = 0
    g_tape_count = 0
    g_boot_start_ms = zyginit_monotonic_ms()
end ui_boot_start

proc ui_tier_start(tier: int)
    g_cur_tier = tier
end ui_tier_start

proc ui_tier_done(tier: int)
    // Header repaint happens on next emit/tick.
end ui_tier_done

proc ui_event_started(name: string, dur_ms: int)
    let elapsed = zyginit_monotonic_ms() - g_boot_start_ms
    g_done = g_done + 1
    tape_push(elapsed, paint("ok", glyph_ok()), name, "")
    emit_or_redraw(elapsed)
end ui_event_started

proc ui_event_failed(name: string, exit_code: int, dur_ms: int)
    let elapsed = zyginit_monotonic_ms() - g_boot_start_ms
    g_done = g_done + 1
    g_failed_count = g_failed_count + 1
    if g_failed_count == 1
        g_failed_first = name
        g_failed_first_exit = exit_code
    end if
    tape_push(elapsed, paint("fail", glyph_fail()), name,
              "exit=" + int_to_str(exit_code))
    emit_or_redraw(elapsed)
end ui_event_failed

// In plain mode: print one line. In rich mode: clear screen and redraw.
proc emit_or_redraw(elapsed_ms: int)
    if g_mode == MODE_PLAIN()
        // Find the most recent tape row and emit it as a plain line.
        if g_tape_count == 0  return  end if
        let last = (g_tape_head - 1) mod TAPE_HEIGHT()
        let line = g_tape[last]
        let level = "info"
        let event = "started"
        // Crude: failure glyph means we render as failed.
        if str.index_of(line.glyph, glyph_fail()) >= 0
            println(fmt_plain_event(elapsed_ms, "err", "failed",
                line.name, line.note))
        else
            println(fmt_plain_event(elapsed_ms, "info", "started",
                line.name, line.note))
        end if
        return
    end if
    // Rich mode: CSI 2J (clear) + CSI H (home).
    print("\x1b[2J\x1b[H")
    print(render_boot_screen(elapsed_ms))
end emit_or_redraw

Add zyginit_monotonic_ms to src/helpers.c:

#include <time.h>
int zyginit_monotonic_ms(void) {
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
    return (int)((ts.tv_sec * 1000) + (ts.tv_nsec / 1000000));
}
  • Step 3: Add boot_tape demo scenario

In ui_demo:

    if scenario == "boot_tape_rich" or scenario == "boot_tape_ascii" or
       scenario == "boot_tape_plain"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        // Hand-rolled event sequence so snapshots are deterministic.
        // Override monotonic_ms by setting g_boot_start_ms to 0 and emitting
        // explicit elapsed values via direct tape_push.
        g_boot_start_ms = 0
        let elapsed_seq = [40, 160, 310, 490, 1040, 1080, 1360, 1550]
        let name_seq = ["root-fs", "crypto", "devfs", "swap",
                        "filesystem", "identity", "sysconfig", "dlmgmtd"]
        mut i = 0
        while i < 8
            g_done = g_done + 1
            tape_push(elapsed_seq[i], paint("ok", glyph_ok()), name_seq[i], "")
            i = i + 1
        end while
        // Inject one failure.
        g_failed_count = 1
        g_failed_first = "network"
        g_failed_first_exit = 1
        g_cur_tier = 5
        g_done = g_done + 1
        tape_push(5020, paint("fail", glyph_fail()), "network", "exit=1")
        // Render once.
        if g_mode == MODE_PLAIN()
            // Plain: re-emit the 9 events.
            let levels = ["info","info","info","info","info","info","info","info","err"]
            let evs    = ["started","started","started","started",
                          "started","started","started","started","failed"]
            let allnames = ["root-fs","crypto","devfs","swap",
                            "filesystem","identity","sysconfig","dlmgmtd","network"]
            let allelap  = [40,160,310,490,1040,1080,1360,1550,5020]
            mut j = 0
            while j < 9
                let kv = ""
                let kv2 = if evs[j] == "failed" then "exit=1" else "" end
                println(fmt_plain_event(allelap[j], levels[j], evs[j],
                                        allnames[j], kv2))
                j = j + 1
            end while
        else
            print(render_boot_screen(5020))
        end if
        return 0
    end if

(Note: the if ... then ... else ... end ternary form may differ in Reef
syntax; if it doesn't compile, fall back to a small helper that returns the
right string.)

  • Step 4: Build, run, sanity-check snapshots
clang -c src/helpers.c -o build/helpers.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/ui_tests.sh

Inspect:

  • boot_tape_rich.txt — should show the 4-row header with truecolor escapes around [Z], the failed: network exit=1 line in red, and a tape with 9 rows. The network row has the glyph in red.
  • boot_tape_ascii.txt — same shape but # and X glyphs, no truecolor escapes (16-color only).
  • boot_tape_plain.txt — 9 plain machine-parseable lines; no escapes.

If the rendered tape is misaligned, fix fmt_tape_row padding before moving on.

  • Step 5: Commit
hg add tests/integration/snapshots/boot_tape_*.txt
hg commit -m "ui: boot header + rolling tape + emit_or_redraw"

Task 5: Progress bar

Files:

  • Modify: src/ui.reef

  • Modify: tests/integration/ui_tests.sh

  • Step 1: Failing snapshot for progress bar

In tests/integration/ui_tests.sh:

check_scenario progress_rich  "TERM=xterm-256color"
check_scenario progress_ascii "TERM=xterm ZYGINIT_ASCII=1"
check_scenario progress_full  "TERM=xterm-256color"   # 17/17 done

Run → FAIL.

  • Step 2: Implement fmt_progress_bar and append to render_boot_screen

Add to src/ui.reef:

fn PROGRESS_WIDTH(): int return 36 end PROGRESS_WIDTH

fn fmt_progress_bar(done: int, total: int): string
    if g_mode == MODE_PLAIN()  return ""  end if
    let width = PROGRESS_WIDTH()
    mut filled = 0
    if total > 0
        filled = (done * width) / total
    end if
    if filled > width  filled = width  end if
    let pct = if total > 0 then (done * 100) / total else 0 end
    mut bar = ""
    if g_mode == MODE_RICH_ASCII()
        // [#########          ] form
        bar = str.concat(bar, "[")
        mut i = 0
        while i < (width - 2)
            if i < ((filled * (width - 2)) / width)
                bar = str.concat(bar, "#")
            else
                bar = str.concat(bar, " ")
            end if
            i = i + 1
        end while
        bar = str.concat(bar, "]")
    else
        mut i = 0
        while i < width
            if i < filled
                bar = str.concat(bar, paint("accent", "█"))
            else
                bar = str.concat(bar, paint("mute", "░"))
            end if
            i = i + 1
        end while
    end if
    return "  " + bar + "  " + int_to_str(done) + "/" + int_to_str(total)
                + "  " + int_to_str(pct) + "%"
end fmt_progress_bar

Modify render_boot_screen to append:

    // Inside render_boot_screen, after the tape loop, before return:
    mut div = ""
    mut k = 0
    while k < 62
        div = str.concat(div, "─")
        k = k + 1
    end while
    out = str.concat(out, "  ")
    out = str.concat(out, paint("accent", div))
    out = str.concat(out, "\n")
    out = str.concat(out, fmt_progress_bar(g_done, g_num_svc))
    out = str.concat(out, "\n")
    return out
  • Step 3: Add progress_* demo scenarios
    if scenario == "progress_rich" or scenario == "progress_ascii"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        g_done = 12
        g_failed_count = 1
        print(fmt_progress_bar(g_done, g_num_svc))
        return 0
    end if
    if scenario == "progress_full"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        g_done = 17
        print(fmt_progress_bar(g_done, g_num_svc))
        return 0
    end if
  • Step 4: Build and inspect
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/ui_tests.sh

Check progress_rich.txt shows █████████████░░░░... 12/17 70%.
Check progress_ascii.txt shows [#################### ] 12/17 70%.
Check progress_full.txt shows x 36 + 17/17 100%.

  • Step 5: Commit
hg add tests/integration/snapshots/progress_*.txt
hg commit -m "ui: progress bar with block + ASCII fallback forms"

Task 6: Spinner + ui_tick

Files:

  • Modify: src/ui.reef

  • Modify: tests/integration/ui_tests.sh

  • Step 1: Failing snapshot for spinner frames

check_scenario spinner_forward  "TERM=xterm-256color"
check_scenario spinner_reverse  "TERM=xterm-256color"
check_scenario spinner_in_tape  "TERM=xterm-256color"

Run → FAIL.

  • Step 2: Implement spinner state

Add to src/ui.reef:

mut g_tick: int = 0
mut g_starting_name: string = ""    // service in STARTING state, if any
mut g_stopping_name: string = ""    // service in STOPPING state, if any

fn N_FRAMES(): int return 4 end N_FRAMES

fn ui_spinner_frame(reverse: bool): string
    let frames = ["/", "-", "\\", "|"]
    if reverse
        return frames[(N_FRAMES() - 1) - (g_tick mod N_FRAMES())]
    end if
    return frames[g_tick mod N_FRAMES()]
end ui_spinner_frame

proc ui_tick()
    g_tick = g_tick + 1
    if g_mode == MODE_PLAIN()  return  end if
    if str.length(g_starting_name) == 0 and str.length(g_stopping_name) == 0
        return
    end if
    // Just redraw the screen on each tick — the tape's last row holds the
    // currently-starting/stopping service, so its glyph updates.
    let elapsed = zyginit_monotonic_ms() - g_boot_start_ms
    print("\x1b[2J\x1b[H")
    print(render_boot_screen(elapsed))
end ui_tick

proc ui_event_starting(name: string)
    g_starting_name = name
    let elapsed = zyginit_monotonic_ms() - g_boot_start_ms
    tape_push(elapsed, paint("warn", ui_spinner_frame(false)), name, "starting")
    emit_or_redraw(elapsed)
end ui_event_starting

proc ui_event_stopping(name: string)
    g_stopping_name = name
    let elapsed = zyginit_monotonic_ms() - g_boot_start_ms
    tape_push(elapsed, paint("warn", ui_spinner_frame(true)), name, "stopping")
    emit_or_redraw(elapsed)
end ui_event_stopping

When ui_event_started or ui_event_failed fires, clear g_starting_name:

// At the top of ui_event_started and ui_event_failed:
g_starting_name = ""

And update render_boot_screen so the active spinner row's glyph reflects the current frame. The cleanest approach: when rendering the tape, if a row's name matches g_starting_name, replace its glyph with paint("warn", ui_spinner_frame(false)); if it matches g_stopping_name, use reverse.

Modify fmt_tape_row to accept and re-paint the spinner row, or — simpler — keep a separate g_active_glyph that ui_tick updates and that the renderer reads when emitting the matching row:

// In fmt_tape_row, before assembling `row`:
mut glyph = line.glyph
if str.length(g_starting_name) > 0 and line.name == g_starting_name
    glyph = paint("warn", ui_spinner_frame(false))
end if
if str.length(g_stopping_name) > 0 and line.name == g_stopping_name
    glyph = paint("warn", ui_spinner_frame(true))
end if
let row = pad + elapsed + "  " + glyph + "  " + line.name

(Update the assembled row accordingly.)

  • Step 3: Add spinner demo scenarios
    if scenario == "spinner_forward"
        g_mode = detect_rich_mode()
        mut i = 0
        while i < 8
            g_tick = i
            print(ui_spinner_frame(false))
            i = i + 1
        end while
        println("")
        return 0
    end if
    if scenario == "spinner_reverse"
        g_mode = detect_rich_mode()
        mut i = 0
        while i < 8
            g_tick = i
            print(ui_spinner_frame(true))
            i = i + 1
        end while
        println("")
        return 0
    end if
    if scenario == "spinner_in_tape"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        g_boot_start_ms = 0
        // 3 done, 1 starting (deterministic frame 2 = "\\")
        tape_push(40, paint("ok", glyph_ok()), "root-fs", "")
        tape_push(160, paint("ok", glyph_ok()), "crypto", "")
        tape_push(310, paint("ok", glyph_ok()), "devfs", "")
        g_done = 3
        ui_event_starting("network")
        g_tick = 2  // force frame "\\"
        print("\x1b[2J\x1b[H")
        print(render_boot_screen(2100))
        return 0
    end if
  • Step 4: Build and verify
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/ui_tests.sh

Sanity:

  • spinner_forward.txt: /-\|/-\|

  • spinner_reverse.txt: |\-/|\-/

  • spinner_in_tape.txt: tape rows show root-fs ●, crypto ●, devfs ●, then network \ (frame index 2) with (starting) note.

  • Step 5: Commit

hg add tests/integration/snapshots/spinner_*.txt
hg commit -m "ui: spinner with reverse rotation for stopping state"

Task 7: Boot final card

Files:

  • Modify: src/ui.reef

  • Modify: tests/integration/ui_tests.sh

  • Step 1: Failing snapshots

check_scenario final_card_ok      "TERM=xterm-256color"
check_scenario final_card_failed  "TERM=xterm-256color"
  • Step 2: Implement ui_boot_complete

Add to src/ui.reef:

type BootStats = struct
    elapsed_ms: int
    online: int
    failed: int
    slowest: [string]      // up to 3 entries: "name N.NNs"
end BootStats

proc ui_boot_complete(stats: BootStats)
    if g_mode == MODE_PLAIN()
        println(fmt_plain_event(stats.elapsed_ms, "info", "boot_complete",
                "summary", "online=" + int_to_str(stats.online)
                + " failed=" + int_to_str(stats.failed)))
        return
    end if
    // Clear screen, paint final card.
    print("\x1b[2J\x1b[H")
    let secs = stats.elapsed_ms / 1000
    let frac = (stats.elapsed_ms mod 1000) / 100
    let elapsed = int_to_str(secs) + "." + int_to_str(frac) + "s"
    let summary_count = int_to_str(stats.online + stats.failed)
    mut out = "  " + sigil() + " zyginit 0.1.3 · " +
              paint("frame", g_runlevel) + "  " +
              paint("accent", "boot " + elapsed) + "\n"
    out = str.concat(out, "  " + paint("frame",
        summary_count + " services — " + int_to_str(stats.online) + " online, "
        + int_to_str(stats.failed) + " failed") + "\n")
    out = str.concat(out, "\n")
    if stats.failed == 0
        out = str.concat(out, "  " + paint("ok", "all services online") + "\n")
    else if stats.failed == 1
        out = str.concat(out, "  " + paint("fail", "failed: " + g_failed_first +
            "  exit=" + int_to_str(g_failed_first_exit)) + "\n")
        out = str.concat(out, "      " +
            paint("accent", "→ zygctl log " + g_failed_first) +
            paint("mute", "    to see why") + "\n")
    else
        out = str.concat(out, "  " + paint("fail",
            "failed: " + int_to_str(stats.failed) + " services") + "\n")
        out = str.concat(out, "      " +
            paint("accent", "→ zygctl status") +
            paint("mute", "    to see them") + "\n")
    end if
    mut div = ""
    mut k = 0
    while k < 62
        div = str.concat(div, "─")
        k = k + 1
    end while
    out = str.concat(out, "  " + paint("accent", div) + "\n")
    if str.length_array(stats.slowest) > 0
        mut slow = "slowest:"
        mut i = 0
        while i < str.length_array(stats.slowest) and i < 3
            slow = str.concat(slow, "  ")
            slow = str.concat(slow, stats.slowest[i])
            i = i + 1
        end while
        out = str.concat(out, "  " + paint("frame", slow) + "\n")
    end if
    print(out)
end ui_boot_complete

(Note: str.length_array may be spelled len() in Reef — verify against ~/repos/reef-lang/docs/. If different, swap to the correct name.)

  • Step 3: Add demo scenarios
    if scenario == "final_card_ok"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        let stats = BootStats{
            elapsed_ms: 8300, online: 17, failed: 0,
            slowest: ["dlmgmtd 1.2s", "filesystem 0.55s", "sshd 0.37s"]
        }
        ui_boot_complete(stats)
        return 0
    end if
    if scenario == "final_card_failed"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        g_failed_first = "network"
        g_failed_first_exit = 1
        let stats = BootStats{
            elapsed_ms: 8300, online: 16, failed: 1,
            slowest: ["network 5.02s", "filesystem 0.55s", "sshd 0.37s"]
        }
        ui_boot_complete(stats)
        return 0
    end if
  • Step 4: Build and verify
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/ui_tests.sh

Inspect both snapshots by hand for layout and color escapes.

  • Step 5: Commit
hg add tests/integration/snapshots/final_card_*.txt
hg commit -m "ui: boot final card (success and failure variants)"

Task 8: Shutdown mirror + final card

Files:

  • Modify: src/ui.reef

  • Modify: tests/integration/ui_tests.sh

  • Step 1: Failing snapshots

check_scenario shutdown_mirror   "TERM=xterm-256color"
check_scenario shutdown_final    "TERM=xterm-256color"
  • Step 2: Implement shutdown procs

Add to src/ui.reef:

mut g_shutdown_reason: string = ""

proc ui_shutdown_start(reason: string)
    g_phase = "shutdown"
    g_shutdown_reason = reason
    g_runlevel = "stopping for " + reason
    g_done = 0       // counts services that have reached STOPPED
    g_failed_count = 0
    g_failed_first = ""
    g_failed_first_exit = 0
    g_tape_head = 0
    g_tape_count = 0
    g_boot_start_ms = zyginit_monotonic_ms()
end ui_shutdown_start

proc ui_event_stopped(name: string, dur_ms: int)
    g_stopping_name = ""
    let elapsed = zyginit_monotonic_ms() - g_boot_start_ms
    g_done = g_done + 1
    tape_push(elapsed, paint("mute", glyph_pending()), name, "")
    emit_or_redraw(elapsed)
end ui_event_stopped

proc ui_shutdown_complete(reason: string, elapsed_ms: int)
    if g_mode == MODE_PLAIN()
        println(fmt_plain_event(elapsed_ms, "info", "shutdown_complete",
                reason, "down=" + int_to_str(g_done)))
        return
    end if
    print("\x1b[2J\x1b[H")
    let secs = elapsed_ms / 1000
    let frac = (elapsed_ms mod 1000) / 100
    let elapsed = int_to_str(secs) + "." + int_to_str(frac) + "s"
    mut out = "  " + sigil() + " zyginit 0.1.3 · " +
              paint("frame", reason + " complete") + "  " +
              paint("accent", reason + " " + elapsed) + "\n"
    let stat = if g_failed_count == 0 then "stopped cleanly"
               else "stopped (" + int_to_str(g_failed_count) +
                    " force-killed)" end
    out = str.concat(out, "  " + paint("frame",
        int_to_str(g_num_svc) + " services " + stat) + "\n")
    mut div = ""
    mut k = 0
    while k < 62
        div = str.concat(div, "─")
        k = k + 1
    end while
    out = str.concat(out, "  " + paint("accent", div) + "\n")
    out = str.concat(out, "  " + paint("mute",
        "invoking uadmin(A_SHUTDOWN, AD_BOOT)") + "\n")
    print(out)
end ui_shutdown_complete

The progress bar already works in shutdown direction because it computes
done / total. Update fmt_progress_bar to show "down" when g_phase == "shutdown":

    let label = if g_phase == "shutdown" then " down" else "" end
    return "  " + bar + "  " + int_to_str(done) + "/" + int_to_str(total)
                + label + "  " + int_to_str(pct) + "%"
  • Step 3: Add demo scenarios
    if scenario == "shutdown_mirror"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        ui_shutdown_start("reboot")
        g_boot_start_ms = 0
        tape_push(40,  paint("mute", glyph_pending()), "sshd", "")
        tape_push(160, paint("mute", glyph_pending()), "console-login", "")
        tape_push(310, paint("mute", glyph_pending()), "cron", "")
        ui_event_stopping("syslogd")
        g_tick = 3   // force last frame "/"
        g_done = 4
        g_cur_tier = 6
        print("\x1b[2J\x1b[H")
        print(render_boot_screen(480))
        return 0
    end if
    if scenario == "shutdown_final"
        g_mode = detect_rich_mode()
        ui_boot_start(17, 8, "multi-user")
        ui_shutdown_start("reboot")
        g_num_svc = 17
        g_done = 17
        ui_shutdown_complete("reboot", 2100)
        return 0
    end if
  • Step 4: Build and verify
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/ui_tests.sh

Inspect:

  • shutdown_mirror.txt — reverse spinner on syslogd row (frame /), header says "stopping for reboot".

  • shutdown_final.txt — final card with "reboot complete" and "stopped cleanly".

  • Step 5: Commit

hg add tests/integration/snapshots/shutdown_*.txt
hg commit -m "ui: shutdown mirror + shutdown final card"

Task 9: zygctl status server-side rendering + PLAIN modifier

Files:

  • Modify: src/ui.reef (add ui_render_status)

  • Modify: src/socket.reef (delegate cmd_status)

  • Modify: tests/integration/run_tests.sh (add tests, set ZYGINIT_NO_UI=1)

  • Step 1: Failing test in the integration harness

In tests/integration/run_tests.sh, add toward the end (after existing tests):

echo "TEST: zygctl status renders banner+table when not piped"
$ZYGINIT &
sleep 0.5
out=$(script -qfc "$ZYGCTL status" /dev/null 2>&1)
echo "$out" | grep -q "\[Z\] zyginit" || fail "expected [Z] sigil in TTY status output"
echo "$out" | grep -q "SERVICE" || fail "expected SERVICE column header"
pass

echo "TEST: zygctl status returns plain when piped"
out=$($ZYGCTL status | cat)
echo "$out" | grep -q "\[Z\]" && fail "did not expect sigil in piped output"
echo "$out" | grep -q "SERVICE" || fail "expected SERVICE column header even in plain"
pass

And ensure ZYGINIT_NO_UI is unset for these specific tests (the boot UI of the daemon is irrelevant — only the rendered status response).

  • Step 2: Run integration suite — confirm failure
./tests/integration/run_tests.sh

Expected: the two new tests fail (sigil absent because the renderer hasn't been wired).

  • Step 3: Add ui_render_status to src/ui.reef
fn ui_render_status(table: supervisor.ServiceTable,
                    want_banner: int, plain: int): string
    let count = supervisor.service_count(table)
    let prev_mode = g_mode
    if plain != 0
        g_mode = MODE_PLAIN()
    end if
    mut out = ""
    // Compute summary counts.
    mut online = 0
    mut failed = 0
    mut i = 0
    while i < count
        let rt = supervisor.get_runtime(table, i)
        let st = supervisor.rt_state(rt)
        if st == supervisor.STATE_RUNNING()
            online = online + 1
        else if st == supervisor.STATE_FAILED() or
                st == supervisor.STATE_MAINTENANCE()
            failed = failed + 1
        end if
        i = i + 1
    end while
    if want_banner != 0 and g_mode != MODE_PLAIN()
        out = str.concat(out, "  " + sigil() + " zyginit 0.1.3 · ")
        out = str.concat(out, paint("frame", "multi-user · " +
            int_to_str(count) + " services · " +
            int_to_str(online) + " online, " +
            int_to_str(failed) + " failed") + "\n")
        mut div = ""
        mut k = 0
        while k < 62
            div = str.concat(div, "─")
            k = k + 1
        end while
        out = str.concat(out, "  " + paint("accent", div) + "\n")
    end if
    // Column header.
    out = str.concat(out, "  ")
    out = str.concat(out, paint("frame",
        pad_right("SERVICE", 14) +
        pad_right("STATE", 11) +
        pad_right("PID", 7) +
        pad_right("CTID", 7) +
        pad_right("UPTIME", 10) +
        "NOTES"))
    out = str.concat(out, "\n")
    // Service rows.
    mut j = 0
    while j < count
        let rt = supervisor.get_runtime(table, j)
        out = str.concat(out, fmt_status_row(rt))
        out = str.concat(out, "\n")
        j = j + 1
    end while
    g_mode = prev_mode
    return out
end ui_render_status

fn fmt_status_row(rt): string
    // Implementation detail: read fields from the runtime struct (consult
    // src/supervisor.reef for accessor names). Output one row:
    //    name        glyph state    pid    ctid    uptime    notes
    // For plain mode: no glyph, no escapes.
    // Re-use the existing supervisor.format_duration() helper for uptime.
    // …pseudo-code body…
    return "  …"
end fmt_status_row

fn pad_right(s: string, n: int): string
    let extra = n - str.length(s)
    if extra <= 0  return s  end if
    mut out = s
    mut i = 0
    while i < extra
        out = str.concat(out, " ")
        i = i + 1
    end while
    return out
end pad_right

Fill in fmt_status_row body using the existing runtime accessors in
src/supervisor.reef:835 (get_status_line). Reuse its logic for
uptime / exit_code / restart_count, but format as columns with the glyph in
the STATE column. State → glyph mapping:

  • STATE_RUNNINGglyph_ok() + " running"

  • STATE_EXITED (oneshot success) → glyph_ok() + " online"

  • STATE_STARTINGglyph_starting() + " starting"

  • STATE_STOPPINGglyph_starting() + " stopping"

  • STATE_STOPPEDglyph_pending() + " stopped"

  • STATE_FAILEDglyph_fail() + " failed"

  • STATE_MAINTENANCEglyph_fail() + " maint."

  • Step 4: Wire cmd_status to delegate

Add a single-row renderer to src/ui.reef (avoids needing a "subtable"
abstraction):

fn ui_render_status_one(table: supervisor.ServiceTable, idx: int,
                        plain: int): string
    let prev_mode = g_mode
    if plain != 0  g_mode = MODE_PLAIN()  end if
    let rt = supervisor.get_runtime(table, idx)
    let row = fmt_status_row(rt) + "\n"
    g_mode = prev_mode
    return row
end ui_render_status_one

Then in src/socket.reef, replace the body of cmd_status (around line 261):

fn cmd_status(table: supervisor.ServiceTable, arg: string): string
    let count = supervisor.service_count(table)
    if str.length(arg) > 0
        let idx = supervisor.find_service(table, arg)
        if idx < 0
            return "error: unknown service: " + arg + "\n"
        end if
        return ui.ui_render_status_one(table, idx, 1)
    end if
    if count == 0
        return "no services loaded\n"
    end if
    return ui.ui_render_status(table, 1, 0)
end cmd_status
  • Step 5: Add the STATUS PLAIN wire modifier

In src/socket.reef, find the request parser (the dispatch around
cmd_status / cmd_list). Adjust to accept a trailing PLAIN token:

// Existing dispatch (rough shape):
//   "STATUS" → cmd_status(table, "")
//   "STATUS sshd" → cmd_status(table, "sshd")
// New:
//   "STATUS PLAIN" → cmd_status_plain(table, "")
//   "STATUS PLAIN sshd" → cmd_status_plain(table, "sshd")
fn cmd_status_plain(table: supervisor.ServiceTable, arg: string): string
    if str.length(arg) > 0
        let idx = supervisor.find_service(table, arg)
        if idx < 0  return "error: unknown service: " + arg + "\n"  end if
        let one_table = supervisor.subtable(table, idx)
        return ui.ui_render_status(one_table, 0, 1)
    end if
    return ui.ui_render_status(table, 0, 1)
end cmd_status_plain

Update the request tokenizer to recognize STATUS PLAIN before falling
through to STATUS.

  • Step 6: Build + run the integration suite
clang -c src/helpers.c -o build/helpers.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
(cd tools/zygctl && clang -c src/symlink_wrapper.c -o build/symlink_wrapper.o && reefc build --obj build/symlink_wrapper.o)
./tests/integration/run_tests.sh

Both new tests should pass; existing 44 should remain green.

  • Step 7: Commit
hg commit -m "ui+socket: zygctl status banner+table + STATUS PLAIN modifier"
  • Step 8: Add ui_render_list and wire cmd_list

zygctl list keeps its minimal one-line-per-service shape but adds a
small enhancement: enabled state, and color on the type chip when not
plain. Add to src/ui.reef:

fn ui_render_list(table: supervisor.ServiceTable, plain: int): string
    let count = supervisor.service_count(table)
    if count == 0  return "no services loaded\n"  end if
    let prev_mode = g_mode
    if plain != 0  g_mode = MODE_PLAIN()  end if
    mut out = ""
    mut i = 0
    while i < count
        let rt = supervisor.get_runtime(table, i)
        let def = supervisor.rt_def(rt)
        let name = config.svc_name(def)
        let stype = config.service_type_name(config.svc_type(def))
        let enabled = supervisor.rt_enabled(rt)
        let chip = paint("accent", stype)
        let enabled_str = if enabled then "enabled" else "disabled" end
        out = str.concat(out, name)
        out = str.concat(out, " (")
        out = str.concat(out, chip)
        out = str.concat(out, ", ")
        out = str.concat(out, enabled_str)
        out = str.concat(out, ")\n")
        i = i + 1
    end while
    g_mode = prev_mode
    return out
end ui_render_list

If supervisor.rt_enabled doesn't exist, fall back to true (every
service in the table is enabled, since that's how it got loaded).

In src/socket.reef, replace cmd_list body:

fn cmd_list(table: supervisor.ServiceTable): string
    return ui.ui_render_list(table, 0)
end cmd_list

fn cmd_list_plain(table: supervisor.ServiceTable): string
    return ui.ui_render_list(table, 1)
end cmd_list_plain

Add LIST PLAIN to the request tokenizer alongside STATUS PLAIN.

  • Step 9: Build + verify list rendering
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
./tests/integration/run_tests.sh

The existing zygctl list tests should still pass (the response now
contains (type, enabled) instead of (type) — if any test does an exact
string match, update it to match the new shape).

  • Step 10: Commit
hg commit -m "ui+socket: zygctl list enabled chip + LIST PLAIN modifier"

Task 10: zygctl client pre-flight + STATUS PLAIN / LIST PLAIN dispatch

Files:

  • Modify: tools/zygctl/src/main.reef

  • Step 1: Failing assertion is already in place from Task 9

(Both tests added in Task 9 still need the client side to dispatch correctly. The server side renders correctly only if asked.)

  • Step 2: Add detect_rich_mode equivalent in zygctl client

In tools/zygctl/src/main.reef, add near the top (above cmd_status /
client dispatch):

extern "C" fn zyginit_isatty(fd: int): int

fn client_should_be_plain(): bool
    if env.has_env("NO_COLOR")  return true  end if
    if zyginit_isatty(1) == 0   return true  end if
    let term = env.get_env_or("TERM", "")
    if str.length(term) == 0    return true  end if
    if term == "dumb"           return true  end if
    return false
end client_should_be_plain

Note: zygctl already links symlink_wrapper.c. Add the zyginit_isatty
function to that same file (it's a thin POSIX call, no special wiring):

#include <unistd.h>
int zyginit_isatty(int fd) {
    return isatty(fd) ? 1 : 0;
}
  • Step 3: Update the status dispatch in tools/zygctl/src/main.reef

Find where status builds its socket request (~line 60 area, exact line
depends on current rev). Replace with:

    if cmd == "status"
        mut req = "STATUS"
        if client_should_be_plain()
            req = "STATUS PLAIN"
        end if
        if str.length(arg) > 0
            req = req + " " + arg
        end if
        send_to_socket(req)
        return
    end if
    if cmd == "list"
        mut req = "LIST"
        if client_should_be_plain()
            req = "LIST PLAIN"
        end if
        send_to_socket(req)
        return
    end if

(send_to_socket is whatever helper the existing code uses for the same
purpose — keep the same call site.)

  • Step 4: Build + run
(cd tools/zygctl && clang -c src/symlink_wrapper.c -o build/symlink_wrapper.o && reefc build --obj build/symlink_wrapper.o)
./tests/integration/run_tests.sh

The two zygctl status tests from Task 9 should now both pass.

  • Step 5: Commit
hg commit -m "zygctl: TTY-aware dispatch (STATUS vs STATUS PLAIN)"

Task 11: Wire ui_* calls into main.reef and supervisor.reef

Files:

  • Modify: src/main.reef

  • Modify: src/supervisor.reef

  • Step 1: Run the existing integration suite — capture baseline

./tests/integration/run_tests.sh 2>&1 | tail -5

Should currently report 46 passing (44 original + 2 from Task 9).

  • Step 2: In src/main.reef, replace the boot-banner prints with ui calls

Find the section in src/main.reef:main() where the daemon prints
zyginit v0.1.3 starting, running as PID 1, booting into multi-user,
loaded N services, etc. Replace with:

    let rich_mode = ui.detect_rich_mode()
    ui.ui_init(rich_mode)

    // Existing dep graph loading happens here…
    let svc_count = supervisor.service_count(g_table)
    let num_tiers = depgraph.tier_count(g_boot_order)
    let runlevel = if g_single_user then "single-user" else "multi-user" end
    ui.ui_boot_start(svc_count, num_tiers, runlevel)

Keep the existing zyginit: … warning/error prints — those route to log
files in rich mode (or stay on-console in plain mode, which is fine).

  • Step 3: Wrap each tier in ui_tier_start / ui_tier_done

Find the tier-iteration loop (for each tier in g_boot_order …). Insert
calls:

    mut tier = 0
    while tier < num_tiers
        ui.ui_tier_start(tier)
        // …existing per-tier service start logic…
        ui.ui_tier_done(tier)
        tier = tier + 1
    end while
  • Step 4: Add ui_tick to the main poll loop

In the steady-state poll loop, after poll() returns:

    ui.ui_tick()

This is cheap (no-op once boot completes and there are no STARTING/STOPPING
services).

  • Step 5: Call ui_boot_complete once all tiers finish

After the tier loop and before "entering event loop":

    let stats = build_boot_stats(g_table, ...)  // collect online/failed/slowest
    ui.ui_boot_complete(stats)

build_boot_stats is a small helper that scans the service table and
computes online count, failed count, and the 3 slowest services by start
time. Add it inline in main.reef.

  • Step 6: Wire shutdown

Find the existing shutdown_services / shutdown sequence. Wrap:

    ui.ui_shutdown_start(g_shutdown_reason)
    // …existing reverse-tier stop loop…
    ui.ui_shutdown_complete(g_shutdown_reason, total_elapsed_ms)

For each stop, in supervisor.reef (next step), ui_event_stopping and
ui_event_stopped will fire.

  • Step 7: In src/supervisor.reef, route lifecycle prints

Find these lines (approximate refs from rev 122):

  • supervisor: started ${name} near line 456 → replace with ui.ui_event_started(name, dur_ms)
  • supervisor: stopping ${name} (signal) near line 490 → replace with ui.ui_event_stopping(name)
  • supervisor: ${name} stopped near line 622 → replace with ui.ui_event_stopped(name, dur_ms)
  • supervisor: ${name} stopped (exit ${code}) near line 653 → replace with ui.ui_event_failed(name, exit_code, dur_ms) when exit_code != 0
  • supervisor: ${name} completed (oneshot) near line 610 → ui.ui_event_started(name, dur_ms) (oneshot completion is "online")
  • supervisor: ${name} failed (oneshot, exit N) near line 613 → ui.ui_event_failed(...)

Add import ui at the top of src/supervisor.reef.

For warning prints (supervisor: ${name} stop timed out, killing,
entered maintenance, scheduled for restart, etc.) — keep them as
println for now. They land in the log file under rich mode and on the
console under plain mode.

  • Step 8: Build and run integration suite
clang -c src/helpers.c -o build/helpers.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o
(cd tools/zygctl && reefc build --obj build/symlink_wrapper.o)
./tests/integration/run_tests.sh

Expected: 46 pass. The integration harness sets ZYGINIT_NO_UI=1 (added in
Task 12 below — set it manually for this run if needed), forcing plain
mode, so the assertions on string outputs don't shift.

  • Step 9: Run ./tests/integration/ui_tests.sh

All previous snapshots should still match.

  • Step 10: Commit
hg commit -m "main+supervisor: route lifecycle prints through ui.reef"

Task 12: Version bump, integration test hygiene, hh-prototest smoke

Files:

  • Modify: tests/integration/run_tests.sh (set ZYGINIT_NO_UI=1)

  • Run: scripts/bump-version.sh 0.1.3

  • Modify: spec/plan dates / status if needed

  • Step 1: Force plain mode in the integration harness

In tests/integration/run_tests.sh, near the existing export ZYGINIT_*
block:

export ZYGINIT_NO_UI=1

This keeps the boot UI from polluting integration-test output. The two
zygctl status tests added in Task 9 explicitly unset this var before
testing the rich vs plain dispatch.

  • Step 2: Verify the integration harness still passes end-to-end
./tests/integration/run_tests.sh
./tests/integration/ui_tests.sh

Both should pass.

  • Step 3: Bump version 0.1.2 → 0.1.3
./scripts/bump-version.sh 0.1.3
hg diff --stat

Expected: changes in reef.toml, tools/zygctl/reef.toml,
tools/sysv-wrapper/Makefile, src/version.reef,
tools/zygctl/src/version.reef. Verify with:

grep -nE 'version|VERSION' reef.toml tools/zygctl/reef.toml \
    src/version.reef tools/zygctl/src/version.reef

All should show 0.1.3.

  • Step 4: Update spec doc-string references in src/ui.reef

Any hardcoded 0.1.3 strings in the demo scenarios are intentional —
verify they still match by running the snapshot suite. If the snapshots
were captured at 0.1.2, the version-string change will produce diffs.
Re-run with snapshot regeneration:

rm tests/integration/snapshots/*.txt
./tests/integration/ui_tests.sh   # regenerates
hg diff tests/integration/snapshots/ | head -50

Confirm the only changes are 0.1.2 → 0.1.3 in the version strings.
Commit the regenerated snapshots.

  • Step 5: Hammerhead deploy + smoke test on hh-prototest
# From dev host:
scp src/ui.reef src/helpers.c src/main.reef src/supervisor.reef \
    src/socket.reef tools/zygctl/src/main.reef tools/zygctl/src/symlink_wrapper.c \
    root@192.168.122.50:/root/zyginit/src/  # adjust paths per file

# Or rsync the whole tree:
rsync -av --exclude build/ . root@192.168.122.50:/root/zyginit/

# On hh-prototest:
ssh root@192.168.122.50 <<'EOF'
    cd /root/zyginit
    gcc -c src/helpers.c -o build/helpers.o
    reefc build -l contract --obj build/helpers.o
    cp build/zyginit /sbin/init.new
    mv /sbin/init.new /sbin/init
    cd tools/zygctl
    gcc -c src/symlink_wrapper.c -o build/symlink_wrapper.o
    reefc build --obj build/symlink_wrapper.o
    cp build/zygctl /sbin/zygctl.new
    mv /sbin/zygctl.new /sbin/zygctl
EOF

# Reboot the VM (init 6 from PID-1 zyginit is a real reboot now):
virsh --connect=qemu:///system reset hh-prototest

# Watch the boot via virsh console (or video out if you can capture):
virsh --connect=qemu:///system console hh-prototest

Verify visually:

  • The boot screen shows the [Z] sigil, rolling tape, and progress bar
    in 256-color (Hammerhead framebuffer = TERM=sun-color).
  • After all tiers complete, the final card appears with boot N.Ns and a
    summary line.
  • console-login overwrites the card with the Login: prompt.
  • Once logged in: zygctl status shows the banner+table in 256-color.

If anything looks wrong, capture screenshots and iterate. Note that polled
I/O is slow — pauses of ~100ms per redraw are expected and not a regression.

  • Step 6: Test shutdown

From the SSH session on hh-prototest:

zygctl reboot

Expected screen: "stopping for reboot" header, services tick from ·
in reverse tier order, final card "reboot complete · stopped cleanly", then
the kernel reset takes over.

  • Step 7: Commit and tag
hg commit -m "release: bump to 0.1.3 (visual pass)"
hg tag v0.1.3
  • Step 8: Update auto-memory

After deploy succeeds, the user will likely want a memory note. The plan
ends here; the user updates MEMORY.md and adds a visual_pass.md memory
file if they want to record the milestone.


Self-review notes

  • Spec coverage: every section of the design spec has at least one task implementing it. §4.3 spinner → Task 6. §5.1 boot tape → Task 4. §5.2 final card → Task 7. §5.3 shutdown mirror → Task 8. §5.5 zygctl status → Task 9 + 10. §6 render policy → Task 2 (detect_rich_mode). §7 error handling → covered inline in emit_or_redraw and the g_failed flag. §8 data flow → Task 11.
  • No placeholders: every step has a concrete file path, code block, or shell command.
  • Type consistency: BootStats defined in Task 7; TapeLine in Task 4; both used by name in later tasks.
  • Tests-first: every task starts with a failing test (snapshot or integration).
  • Frequent commits: every task ends with a focused hg commit.
  • YAGNI: no speculative features. Spinner is event-driven, not timer-driven. No tier_name field. No graphical splash.
  • DRY: paint, glyph_*, fmt_plain_event, pad_right factored once; reused everywhere.