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 teststests/integration/ui_tests.sh— shell-based snapshot test runner
Modify:
src/helpers.c— addzyginit_isatty(int fd)(returns 1/0)src/main.reef— wireui_init,ui_boot_start,ui_tier_start/done,ui_boot_complete,ui_shutdown_start/complete,ui_tick; add--ui-demo <scenario>flag dispatchsrc/supervisor.reef— replace ~8 lifecycleprintlnsites withui_event_*callssrc/socket.reef—cmd_status/cmd_listdelegate toui_render_*; parsePLAINmodifiertools/zygctl/src/main.reef— pre-flightdetect_rich_mode; sendSTATUSorSTATUS PLAINtests/integration/run_tests.sh—export ZYGINIT_NO_UI=1for diff stabilityreef.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 existingscripts/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(appendzyginit_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_isattyC 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.reefskeleton
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-demointosrc/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_modeand plain-mode emitter insrc/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=0detect_dumb.txt:mode=0detect_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.txtcontains\x1b[38;2;truecolor escapes. -
palette_16.txtcontains\x1b[37metc — no truecolor. -
glyphs_unicode.txt:● ◉ ✕ · -
glyphs_ascii.txt:# o X . -
sigil_rich.txtstarts 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], thefailed: network exit=1line in red, and a tape with 9 rows. Thenetworkrow has the✕glyph in red.boot_tape_ascii.txt— same shape but#andXglyphs, 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_barand append torender_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 showroot-fs ●,crypto ●,devfs ●, thennetwork \(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 onsyslogdrow (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(addui_render_status) -
Modify:
src/socket.reef(delegatecmd_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_statustosrc/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_RUNNING→glyph_ok()+ " running" -
STATE_EXITED(oneshot success) →glyph_ok()+ " online" -
STATE_STARTING→glyph_starting()+ " starting" -
STATE_STOPPING→glyph_starting()+ " stopping" -
STATE_STOPPED→glyph_pending()+ " stopped" -
STATE_FAILED→glyph_fail()+ " failed" -
STATE_MAINTENANCE→glyph_fail()+ " maint." -
Step 4: Wire
cmd_statusto 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 PLAINwire 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_listand wirecmd_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_modeequivalent 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
statusdispatch intools/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_tickto 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_completeonce 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 withui.ui_event_started(name, dur_ms)supervisor: stopping ${name} (signal)near line 490 → replace withui.ui_event_stopping(name)supervisor: ${name} stoppednear line 622 → replace withui.ui_event_stopped(name, dur_ms)supervisor: ${name} stopped (exit ${code})near line 653 → replace withui.ui_event_failed(name, exit_code, dur_ms)when exit_code != 0supervisor: ${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(setZYGINIT_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.Nsand a
summary line. console-loginoverwrites the card with theLogin:prompt.- Once logged in:
zygctl statusshows 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 inemit_or_redrawand theg_failedflag. §8 data flow → Task 11. - No placeholders: every step has a concrete file path, code block, or shell command.
- Type consistency:
BootStatsdefined in Task 7;TapeLinein 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_namefield. No graphical splash. - DRY:
paint,glyph_*,fmt_plain_event,pad_rightfactored once; reused everywhere.