|
root / docs / superpowers / plans / 2026-06-01-progress-in-divider.md
2026-06-01-progress-in-divider.md markdown 801 lines 30.3 KB

Progress Gauge in Header Divider — 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: Relocate the boot/shutdown progress indicator from the last row of the screen into the existing top divider of the boot header, fixing the disappear-during-scroll artifact and reclaiming two rows of vertical tape space.

Architecture: All changes are local to src/ui.reef. A new pure function fmt_divider_gauge(done, total) renders the gauge as part of the divider. fmt_header() calls it instead of make_divider() for its trailing rule. render_boot_screen() drops its bottom divider and bottom progress bar (those rows are reclaimed by the tape). ui_boot_complete() and ui_shutdown_complete() already exist and continue to paint their final cards unchanged; the gauge only runs during the live boot/shutdown screen.

Tech Stack: Reef 0.4.0+, GCC/Clang for helpers.c, Mercurial (hg) for version control, manual visual validation via the existing --ui-demo <scenario> scenario harness in ui_demo() at src/ui.reef:852.

Spec: docs/superpowers/specs/2026-06-01-progress-in-divider-design.md (rev 188, amended rev 189, simplified rev 190).

Build commands (used throughout):

  • Linux dev (with stubs — verifies rendering, not contracts):

    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
    

    Result: build/zyginit executable.

  • Hammerhead (real binary for hh-prototest deploy):

    gcc -c src/helpers.c -o build/helpers.o
    reefc build -l contract --obj build/helpers.o
    
  • Run a scenario:

    ./build/zyginit --ui-demo <scenario_name>
    

VCS: Mercurial. hg add <file> for new files, hg commit -m "<msg>". Do not use git.


Task 1: Add gauge scenarios to the demo harness (failing test)

Files:

  • Modify: src/ui.reef:950-963 (the existing progress_* scenario blocks inside ui_demo())

The existing scenarios at lines 950-963 call fmt_progress_bar(g_done, g_num_svc). Don't touch those yet — Task 5 will rewrite them. Instead, add new scenarios to the same ui_demo chain so the build will fail until fmt_divider_gauge exists.

  • Step 1: Add three new scenarios just after the existing progress_full block at line 963.

Insert before the if scenario == "spinner_forward" block:

    if scenario == "gauge_live_31" or scenario == "gauge_live_31_ascii"
        if scenario == "gauge_live_31_ascii"
            g_mode = MODE_RICH_ASCII()
        else
            g_mode = detect_rich_mode()
            if g_mode == MODE_PLAIN()  g_mode = MODE_RICH_TRUE()  end if
        end if
        ui_detect_winsize()
        println(fmt_divider_gauge(31, 100))
        return 0
    end if
    if scenario == "gauge_live_100"
        g_mode = detect_rich_mode()
        if g_mode == MODE_PLAIN()  g_mode = MODE_RICH_TRUE()  end if
        ui_detect_winsize()
        println(fmt_divider_gauge(100, 100))
        return 0
    end if
    if scenario == "gauge_live_0"
        g_mode = detect_rich_mode()
        if g_mode == MODE_PLAIN()  g_mode = MODE_RICH_TRUE()  end if
        ui_detect_winsize()
        println(fmt_divider_gauge(0, 100))
        return 0
    end if

Three scenarios: 31% (representative live state), 100% (boundary), 0% (boundary). gauge_live_31_ascii exercises the MODE_RICH_ASCII branch. The if g_mode == MODE_PLAIN() ... = MODE_RICH_TRUE() override is needed because demo runs on a non-TTY (when piped to cat for inspection) and otherwise short-circuits to plain mode — same pattern used by the existing progress_* scenarios indirectly via detect_rich_mode().

  • Step 2: Run the Linux dev build to verify it fails.
cd /home/ctusa/repos/zygaena-project/zyginit
clang -c src/helpers.c -o build/helpers.o
clang -c src/contract_linux_stubs.c -o build/contract_linux_stubs.o
reefc build --obj build/helpers.o --obj build/contract_linux_stubs.o

Expected: compile or link error referencing fmt_divider_gauge not defined (Reef reports this at type-check stage). If the build accidentally succeeds, the scenario string was probably mistyped — re-check the inserted block.

  • Step 3: Commit the failing test.
hg commit -m "test(ui): add gauge_live_* scenarios for fmt_divider_gauge (build fails)

Three new scenarios in ui_demo:
- gauge_live_31, gauge_live_31_ascii — representative live state, both
  rich-color and rich-ascii rendering paths
- gauge_live_100 — full-bar boundary
- gauge_live_0  — empty-bar boundary

Build fails because fmt_divider_gauge does not exist yet."

Task 2: Implement fmt_divider_gauge (make scenarios pass)

Files:

  • Modify: src/ui.reef — add export declaration and function body. The function replaces both make_divider() (semantically) and fmt_progress_bar() (visually).

  • Step 1: Add the export declaration.

Edit src/ui.reef line 54 area (the export block). Insert a new line just after fn fmt_progress_bar(done: int, total: int): string:

    fn fmt_divider_gauge(done: int, total: int): string

So lines 54-55 read:

    fn fmt_progress_bar(done: int, total: int): string
    fn fmt_divider_gauge(done: int, total: int): string

(We'll delete fmt_progress_bar in Task 5; for now both coexist.)

  • Step 2: Implement the function. Insert it immediately before fmt_progress_bar at line 393.
// Render the boot/shutdown header's bottom rule as a divider with an
// embedded progress gauge. Replaces both make_divider() (in fmt_header)
// and fmt_progress_bar() (in render_boot_screen). Total visual width =
// g_line_width - 16 (matches make_divider). Brackets + 26-cell inner
// content + equal lpad/rpad of dashes. Narrow-terminal fallback drops
// the brackets and shows just "── NN% ──".
fn fmt_divider_gauge(done: int, total: int): string
    if g_mode == MODE_PLAIN()  return ""  end if

    let total_width = g_line_width - 16
    let bar_width = 20
    let inner_width = 26              // bar(20) + "  "(2) + "NNN%"(4)
    let bracket_width = inner_width + 2   // 28: '[' + inner + ']'

    mut filled = 0
    if total > 0
        filled = (done * bar_width) / total
    end if
    if filled > bar_width  filled = bar_width  end if
    if filled < 0  filled = 0  end if

    mut pct = 0
    if total > 0
        pct = (done * 100) / total
    end if
    if pct > 100  pct = 100  end if
    if pct < 0  pct = 0  end if

    // pct_field: right-aligned 3 cells + '%' = 4 cells. E.g. "  3%", " 31%", "100%".
    let pct_str = int_to_str(pct)
    let pct_len = str.length(pct_str)
    mut pct_field = ""
    mut pp = 0
    while pp < (3 - pct_len)
        pct_field = str.concat(pct_field, " ")
        pp = pp + 1
    end while
    pct_field = str.concat(pct_field, pct_str)
    pct_field = str.concat(pct_field, "%")

    // Narrow-terminal fallback: not enough room for brackets + padding.
    if total_width < bracket_width + 2
        // Emit "── NN% ──" centered in total_width.
        let label = str.concat(" ", str.concat(pct_field, " "))
        let label_len = 1 + 4 + 1   // " " + "NNN%" + " "
        mut pad = 0
        if total_width > label_len
            pad = total_width - label_len
        end if
        let lpad_n = pad / 2
        let rpad_n = pad - lpad_n
        mut out = ""
        mut a = 0
        while a < lpad_n
            out = str.concat(out, "─")
            a = a + 1
        end while
        out = str.concat(out, label)
        a = 0
        while a < rpad_n
            out = str.concat(out, "─")
            a = a + 1
        end while
        return paint("accent", out)
    end if

    // Full layout: lpad + '[' + bar + "  " + pct_field + ']' + rpad
    let pad_total = total_width - bracket_width
    let lpad_n = pad_total / 2
    let rpad_n = pad_total - lpad_n

    // Build the bar. Painting per-cell to support per-mode chars.
    mut bar = ""
    mut i = 0
    while i < bar_width
        if g_mode == MODE_RICH_ASCII()
            if i < filled
                bar = str.concat(bar, "#")
            else
                bar = str.concat(bar, " ")
            end if
        else
            if i < filled
                bar = str.concat(bar, paint("accent", "█"))
            else
                bar = str.concat(bar, paint("mute", "░"))
            end if
        end if
        i = i + 1
    end while

    // Assemble. Brackets, pct, and pad chars all painted "accent".
    mut out = ""
    mut j = 0
    while j < lpad_n
        out = str.concat(out, "─")
        j = j + 1
    end while
    out = paint("accent", out)
    out = str.concat(out, paint("accent", "["))
    out = str.concat(out, bar)
    out = str.concat(out, "  ")
    out = str.concat(out, paint("accent", pct_field))
    out = str.concat(out, paint("accent", "]"))
    mut rpad = ""
    j = 0
    while j < rpad_n
        rpad = str.concat(rpad, "─")
        j = j + 1
    end while
    out = str.concat(out, paint("accent", rpad))
    return out
end fmt_divider_gauge

Notes for the implementer:

  • paint() returns the input text unchanged when the mode doesn't support color (e.g., MODE_RICH_ASCII); the ANSI escapes are only added in MODE_RICH_16 / MODE_RICH_TRUE. So calling paint("accent", "─") is safe across modes.

  • The "─" character (U+2500) is used in all rich modes including MODE_RICH_ASCII, matching the pre-existing make_divider() behavior. (make_divider does not differentiate modes.) Only the bar fill character differentiates rich-ascii (#/space) from rich-color (/).

  • str.length() returns the byte length of the string. For ASCII content (pct_field), byte length equals visual cell count — safe.

  • The narrow-terminal fallback is exercised only when total_width < 30 (i.e., g_line_width < 46). At the project's minimum g_line_width = 40 (clamped in ui_detect_winsize at line 691-693), total_width = 24, which does hit the fallback. Verify visually.

  • Step 3: Rebuild.

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

Expected: clean build.

  • Step 4: Run the three new scenarios and visually inspect.
./build/zyginit --ui-demo gauge_live_31
./build/zyginit --ui-demo gauge_live_31_ascii
./build/zyginit --ui-demo gauge_live_100
./build/zyginit --ui-demo gauge_live_0

Expected output shapes (terminal width 80, so g_line_width = 78, total_width = 62):

  • gauge_live_31 — colored 8-cell fill out of 20, 31% label:
    ─────────────────[████████░░░░░░░░░░░░  31%]─────────────────
    
  • gauge_live_31_ascii — ASCII #/space, same layout, no ANSI escapes:
    -----------------[########            31%]-----------------
    
    (Hyphens shown here for clarity — the actual output uses . Run with | cat -v to see escape sequences are absent.)
  • gauge_live_100 — full bar, 100%:
    ─────────────────[████████████████████  100%]─────────────────
    
  • gauge_live_0 — empty bar, 0%:
    ─────────────────[░░░░░░░░░░░░░░░░░░░░    0%]─────────────────
    

If output looks off, inspect by piping through cat -v to see escape sequences explicitly. Common issues to check: bar width (must be exactly 20 cells of fill+empty), pct field width (must be exactly 4 cells: 3-digit number + %), brackets present, total visual width consistent across scenarios.

  • Step 5: Commit.
hg commit -m "feat(ui): implement fmt_divider_gauge

Renders the boot/shutdown header rule as a divider with embedded
progress gauge: [bar  NN%] flanked by '─' padding. Falls back to a
narrow-terminal layout (no brackets, just '── NN% ──') when
g_line_width < 46. ASCII mode uses #/space for fill; rich mode uses
█/░ painted accent/mute."

Task 3: Wire fmt_divider_gauge into fmt_header

Files:

  • Modify: src/ui.reef:357-389 (fmt_header function)

The header today ends with a cosmetic divider from make_divider(). Replace that call with fmt_divider_gauge(g_done + g_skipped, g_num_svc). Also add the elapsed-suppress conditional.

  • Step 1: Update fmt_header to suppress the elapsed Xs field once boot has visually completed.

Find lines 365-366 in src/ui.reef:

    out = str.concat(out, "   ")
    out = str.concat(out, paint("accent", str.concat("elapsed ", elapsed)))

Wrap them in a phase-aware conditional. Replace with:

    let boot_visually_done = (g_phase == "boot") and
                             ((g_done + g_skipped + g_failed_count) >= g_num_svc) and
                             (g_num_svc > 0)
    if not boot_visually_done
        out = str.concat(out, "   ")
        out = str.concat(out, paint("accent", str.concat("elapsed ", elapsed)))
    end if

Reef uses not, and, or for boolean operators (verified at src/ui.reef:527 if not has_active_tape_row() and line 869 or scenario ==). The == / >= operators are numeric comparison as used at line 375.

  • Step 2: Replace the trailing make_divider() call with the new gauge.

Find lines 386-387:

    out = str.concat(out, "\n  ")
    out = str.concat(out, paint("accent", make_divider()))

Replace with:

    out = str.concat(out, "\n  ")
    out = str.concat(out, fmt_divider_gauge(g_done + g_skipped, g_num_svc))

Note: fmt_divider_gauge returns already-painted text — do not wrap in paint("accent", ...).

  • Step 3: Rebuild and exercise the existing full-header scenarios.
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
./build/zyginit --ui-demo boot_tape_rich

Expected: the four-line header now ends with the gauge divider (e.g. ─────[███...░░░ 47%]──────) instead of a plain ───────── line. The bottom of the screen still shows the old divider+progress_bar pair — those go in Task 4. The elapsed Xs field on header line 1 should still be present (the scenario doesn't reach 100%).

  • Step 4: Verify the elapsed-suppress branch.

Run boot_tape_ascii (ends at 9/9 in some scenarios) or check final_card_ok — though the latter exits through ui_boot_complete() which doesn't call fmt_header. Easiest verification: temporarily add a one-off scenario that calls ui_boot_start(5, 1, "multi-user"), sets g_done = 5, then print(fmt_header(5000)). Confirm header line 1 omits elapsed Xs. Roll that scenario back before commit (it's a debug aid, not part of the deliverable).

  • Step 5: Commit.
hg commit -m "feat(ui): wire fmt_divider_gauge into header trailing rule

fmt_header() now renders its trailing rule via fmt_divider_gauge
instead of make_divider, so the boot screen header carries the live
progress gauge. Also suppress the 'elapsed Xs' field on header line 1
once boot is visually complete (g_done + g_skipped + g_failed_count
>= g_num_svc), avoiding stale-timer visual noise during the brief
window before ui_boot_complete fires the final card."

Task 4: Strip bottom divider and bottom progress bar from render_boot_screen

Files:

  • Modify: src/ui.reef:499-521 (render_boot_screen function)

  • Modify: src/ui.reef:682 (g_tape_height calculation)

  • Step 1: Remove the bottom divider concat and the bottom progress_bar concat.

Find lines 513-520:

        i = i + 1
    end while
    out = str.concat(out, str.concat("  ", paint("accent", make_divider())))
    out = str.concat(out, "\n")
    out = str.concat(out, fmt_progress_bar(g_done + g_skipped, g_num_svc))
    // No trailing newline: total height is exactly g_screen_rows. A trailing
    // \n would advance the cursor past the bottom edge and scroll the [Z]
    // banner off the top of the screen.
    return out

Replace with:

        i = i + 1
    end while
    // No trailing newline: total height is exactly g_screen_rows. A trailing
    // \n would advance the cursor past the bottom edge and scroll the [Z]
    // banner off the top of the screen. The progress gauge lives in the
    // header rule (see fmt_divider_gauge), not on the last row.
    return out

Result: the tape loop is the last content; no trailing divider or progress bar.

  • Step 2: Reduce g_tape_height reserve from 6 to 4.

Find line 681-682:

    // Tape height = screen rows - (header 4 + progress 2) with breathing room
    g_tape_height = g_screen_rows - 6

Replace with:

    // Tape height = screen rows - (header 4 rows including gauge divider).
    // Gauge lives inside the header, not on a separate trailing row.
    g_tape_height = g_screen_rows - 4

Leave the clamps (< 4, > 32) on lines 683-688 unchanged.

  • Step 3: Rebuild and verify full layout.
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
./build/zyginit --ui-demo boot_tape_rich

Expected: 4-row header ending with the gauge divider, followed immediately by the tape rows (no trailing divider, no trailing progress bar). The tape should have 2 more rows than before (because g_tape_height is +2).

  • Step 4: Compare row count visually.

Count rows of the output. On a 25-row default terminal: 4 header rows + 21 tape rows = 25 total. Before the change: 4 header + 19 tape + 1 bottom divider + 1 progress = 25. Either way the total is 25; the change is two more tape slots.

  • Step 5: Commit.
hg commit -m "feat(ui): strip bottom divider + progress bar from render_boot_screen

Both are obsolete now that fmt_header's trailing rule carries the gauge.
Tape height adjusted from g_screen_rows-6 to g_screen_rows-4 to claim
the reclaimed rows.

Fixes the disappearing-progress-bar artifact: the gauge no longer
lives on the volatile last row of the screen."

Task 5: Convert legacy progress_* scenarios; delete dead code

Files:

  • Modify: src/ui.reef:950-963 (legacy scenarios)

  • Modify: src/ui.reef:54 (export of fmt_progress_bar)

  • Modify: src/ui.reef:391-439 (definitions of PROGRESS_WIDTH and fmt_progress_bar)

  • Step 1: Rewrite the three legacy scenarios to use the gauge.

Find lines 950-963:

    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

Replace with:

    if scenario == "progress_rich" or scenario == "progress_ascii"
        if scenario == "progress_ascii"
            g_mode = MODE_RICH_ASCII()
        else
            g_mode = detect_rich_mode()
            if g_mode == MODE_PLAIN()  g_mode = MODE_RICH_TRUE()  end if
        end if
        ui_detect_winsize()
        ui_boot_start(17, 8, "multi-user")
        g_done = 12
        g_failed_count = 1
        println(fmt_divider_gauge(g_done + g_skipped, g_num_svc))
        return 0
    end if
    if scenario == "progress_full"
        g_mode = detect_rich_mode()
        if g_mode == MODE_PLAIN()  g_mode = MODE_RICH_TRUE()  end if
        ui_detect_winsize()
        ui_boot_start(17, 8, "multi-user")
        g_done = 17
        println(fmt_divider_gauge(g_done + g_skipped, g_num_svc))
        return 0
    end if

Same scenario names, same semantic intent (visualize boot progress at 12/17 with one failure, then at 17/17 full), but now using the new gauge. Switched printprintln to match the new gauge scenarios (terminal-friendly).

  • Step 2: Delete PROGRESS_WIDTH() and fmt_progress_bar().

Find lines 391-439 and delete the entire block:

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
    mut pct = 0
    if total > 0
        pct = (done * 100) / total
    end if
    mut bar = ""
    if g_mode == MODE_RICH_ASCII()
        let inner = width - 2
        let inner_filled = (filled * inner) / width
        bar = str.concat(bar, "[")
        mut i = 0
        while i < inner
            if i < inner_filled
                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
    mut label = ""
    if g_phase == "shutdown"  label = " down"  end if
    return str.concat(
        str.concat("  ", bar),
        str.concat(
            str.concat(str.concat("  ", int_to_str(done)),
                       str.concat("/", int_to_str(total))),
            str.concat(label, str.concat("  ", str.concat(int_to_str(pct), "%")))))
end fmt_progress_bar

The block ends at the end fmt_progress_bar line (line 439). Delete everything from fn PROGRESS_WIDTH(): int through end fmt_progress_bar inclusive.

  • Step 3: Remove the export declaration for fmt_progress_bar.

Find line 54:

    fn fmt_progress_bar(done: int, total: int): string

Delete this entire line. The line below (fn fmt_divider_gauge(...)) remains.

  • Step 4: Grep to confirm no remaining callers.
grep -rn "fmt_progress_bar\|PROGRESS_WIDTH" src/

Expected: no output. If any reference remains, that's a real bug — track it down before continuing.

  • Step 5: Rebuild and run all gauge-related scenarios.
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

./build/zyginit --ui-demo gauge_live_0
./build/zyginit --ui-demo gauge_live_31
./build/zyginit --ui-demo gauge_live_31_ascii
./build/zyginit --ui-demo gauge_live_100
./build/zyginit --ui-demo progress_rich
./build/zyginit --ui-demo progress_ascii
./build/zyginit --ui-demo progress_full
./build/zyginit --ui-demo boot_tape_rich
./build/zyginit --ui-demo boot_tape_ascii

Expected: clean build, all scenarios render correctly. boot_tape_* shows the full new layout (header + gauge divider + 8 tape rows, no trailing divider/progress).

  • Step 6: Commit.
hg commit -m "refactor(ui): drop fmt_progress_bar and PROGRESS_WIDTH

Both obsolete after the gauge moved into fmt_divider_gauge. Legacy
progress_rich / progress_ascii / progress_full scenarios converted
to exercise fmt_divider_gauge instead. No remaining callers.

grep -rn 'fmt_progress_bar\\|PROGRESS_WIDTH' src/ → empty."

Task 6: Full Linux dev validation sweep

Files:

  • None modified. Pure validation pass.

  • Step 1: Run every existing UI scenario and confirm nothing regressed.

SCENARIOS=(skeleton plain_boot palette_true palette_16 \
           glyphs_unicode glyphs_ascii sigil_rich \
           boot_tape_rich boot_tape_ascii boot_tape_plain \
           progress_rich progress_ascii progress_full \
           spinner_forward spinner_reverse spinner_in_tape \
           final_card_ok final_card_failed \
           gauge_live_0 gauge_live_31 gauge_live_31_ascii gauge_live_100)

for s in "${SCENARIOS[@]}"; do
    echo "--- $s ---"
    ./build/zyginit --ui-demo "$s"
    echo
done

For each scenario, eyeball the output. Specific things to verify:

  • boot_tape_rich: header ends with gauge divider showing 8/17 → (8*100)/17 = 47% fill.

  • final_card_ok / final_card_failed: unchanged from before (these go through ui_boot_complete, not the live boot screen).

  • boot_tape_plain: still emits plain text lines (no gauge — MODE_PLAIN).

  • Step 2: Verify narrow-terminal fallback.

stty cols 40
./build/zyginit --ui-demo gauge_live_31
stty cols 80   # restore

Expected: gauge renders as ── 31% ──-style narrow form, no brackets, no bar.

If stty cols 40 doesn't propagate to the child process (depends on shell + TTY semantics), an alternative: temporarily edit ui_detect_winsize() to force g_line_width = 24, run the scenario, then revert.

  • Step 3: No commit needed — validation pass, no files changed.

Task 7: Hammerhead build, deploy, and live verification

Files:

  • None modified. Deploy and verify on the test VM.

  • Step 1: Build for Hammerhead from the dev host.

The dev host doesn't have GCC for Hammerhead targets — push source to the VM and build there. Two options: build remotely (preferred — fewer moving parts) or build locally with a cross-compiler. Use remote:

# From dev host:
scp src/ui.reef root@192.168.122.50:/root/zyginit/src/

(Only ui.reef changed.)

  • Step 2: Build on hh-prototest and replace /sbin/init.
ssh root@192.168.122.50 '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'

Expected: clean build (no warnings about unused fmt_progress_bar), file replaced atomically via rename.

  • Step 3: Force a cold reboot via virsh.

init 6 from non-PID-1 zyginit doesn't reboot — it spawns a supervisor-mode child. Use virsh:

virsh --connect=qemu:///system reset hh-prototest
  • Step 4: Observe the boot console via virt-viewer / virsh console.

Open the VM console (virsh --connect=qemu:///system console hh-prototest, or VNC via virt-manager). Expected during boot:

  • 4-row header (sigil line, counts line, failed line, gauge divider)
  • Gauge fills as services start across tiers
  • Tape rows occupy everything below the divider
  • No disappearing-bar artifact — kernel printks (if any) scroll lower content but the gauge stays put in row 4

Once boot completes, the final card appears (existing behavior, unchanged).

  • Step 5: Test shutdown.
ssh root@192.168.122.50 'zygctl halt'

Expected: shutdown screen renders with gauge filling 0→100% as services stop. Once all services are stopped, the shutdown final card replaces the screen ("reboot complete · invoking uadmin..."). System reboots cleanly.

After the reboot, SSH back in and verify who -b and uptime report correct times (sanity check that shutdown didn't corrupt anything).

  • Step 6: Force-scroll test — confirm the bug is fixed.

The original bug surfaces during boot when something else writes to /dev/console between zyginit redraws. Easiest reproducer is the cold-boot path itself (Step 4): kernel printks and driver messages naturally interleave with zyginit's tape updates. Watch the VM console during boot.

Expected: the gauge in the divider stays visible across console churn. It may flicker on redraw (as zyginit clears and repaints), but it does not disappear off the bottom of the screen as the pre-fix bar did. If you want a deliberate stress test, induce a one-shot kernel message in a non-PID-1 supervisor instance:

ssh root@192.168.122.50 '
    ZYGINIT_CONFIG_DIR=/etc/zyginit /sbin/init &
    sleep 1
    echo "test message" > /dev/console
    sleep 1
    kill %1
'

The gauge should remain on row 4 across the echo > /dev/console write.

  • Step 7: Commit the source changes if not already committed and tag.

All file changes were committed in earlier tasks. Final action is a single sanity commit if any final tweaks were needed, then tag:

hg log -l 8 --template '{rev}: {desc|firstline}\n'   # confirm clean history

No tag yet — that's a release decision; leave for the user.


Self-Review

Spec coverage:

  • §1 problem statement → addressed by entire plan, especially Tasks 3–4 (relocating gauge out of last row).
  • §2 goals (stability, no-twitch, reclaim space, shutdown symmetry, no plain/ASCII regression) → all covered.
  • §4 visual mockups → Tasks 2, 3, 4 deliver §4.2; §4.4 covered by the unchanged ui_shutdown_complete plus the gauge running during stop sequence.
  • §5 rendering rules (geometry, ASCII vs rich, narrow fallback) → Task 2.
  • §6 state changes (none) → respected: no new g_* flags introduced.
  • §7 files touched (only src/ui.reef) → all task edits target src/ui.reef.
  • §8 backwards compat (plain mode unchanged) → preserved (MODE_PLAIN() early-out in fmt_divider_gauge); legacy progress_* scenarios rewritten to use new gauge but keep the same scenario names so any external test scripts continue to find them.
  • §9 testing — manual on hh-prototest → Task 7.

Placeholder scan: No "TBD", no "implement later", no "similar to Task N", no skeleton steps. Every code block is complete.

Type consistency: fmt_divider_gauge(done: int, total: int): string used consistently in export declaration, function body, and three caller sites (Tasks 1, 3, 5). g_done, g_skipped, g_failed_count, g_num_svc, g_phase, g_mode all reference existing state in src/ui.reef. MODE_PLAIN, MODE_RICH_ASCII, MODE_RICH_TRUE, detect_rich_mode, ui_detect_winsize, ui_boot_start, paint, str.concat, str.length, int_to_str all exist (verified by grep). paint() signature is (token: string, text: string): string.

Scope: Single focused UI change. One file. Seven tasks of escalating commitment (test → impl → wire → strip → cleanup → validate → deploy). Self-contained.