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

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

  ```bash
  gcc -c src/helpers.c -o build/helpers.o
  reefc build -l contract --obj build/helpers.o
  ```

- **Run a scenario:**

  ```bash
  ./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:

```reef
    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.**

```bash
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.**

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

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

So lines 54-55 read:

```reef
    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.**

```reef
// 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.**

```bash
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.**

```bash
./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.**

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

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

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

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

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

Replace with:

```reef
    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.**

```bash
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.**

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

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

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

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

Replace with:

```reef
    // 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.**

```bash
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.**

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

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

```reef
    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 `print` → `println` 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:

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

```reef
    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.**

```bash
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.**

```bash
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.**

```bash
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.**

```bash
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.**

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

```bash
# 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`.**

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

```bash
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.**

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

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

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