Browse Source

feat(skills): bake terminal panel design system v1

DESIGN.md is now a comprehensive design-spec document modelled on
google-labs-code/design.md, covering vision, principles, foundations
(color tokens, glyph palette, invisible grid), 14 components, 6
patterns, edge cases, anti-patterns, the term.sh implementation
contract, and a §11 diagram language with 8 composed patterns and
a 16-element architecture exemplar.

term.sh implements the spec:

  Foundations
    term_init, term_color (green/yellow/orange/red/cyan/magenta/dim)
    Registries: TERM_BRAND, TERM_HEALTH_GLYPH, TERM_DIAGRAM_ICON

  Panel chrome
    term_panel_open  emoji_key name [right_indicator]
    term_panel_close [hotkeys] [health_indicators]
    term_panel_vert
    Glyph palette: ╭ ╰ ─ ●  (rounded corners, terminator dot)

  Body components
    term_section <state> <label> <count>
    term_summary_line <text>
    term_leaf_line <connector> <name> <leaf_glyph> <meta> <age>
    term_toast <emoji_key> <text>
    term_alert <severity> <text>           # ▲ orange/red sub-row

  Leaf glyph builders (one style per panel)
    term_rail <commits_ahead> <head_state>     # ●─●─●─◉ / ●─●─⊗
    term_pip_bar <metric_type> <filled> <total> # progress|score|capacity

  Right-side furniture
    term_health <state> <text>             # • daemon (⬤ for busted)
    term_hotkey <key> <verb>

  Live mode
    term_spinner_frame working|heartbeat <tick>
      working:   ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏  (10-frame braille)
      heartbeat: · ∙ • ● • ∙           (6-frame pulse for daemon)

  Edge cases
    term_truncate, NO_COLOR / TERM_ASCII / FORCE_COLOR honored
    Every Unicode glyph has an ASCII proxy registered

fleet-ops cmd_fleet rewritten to use the panel grammar:
  - Panel chrome via term_panel_open/close with brand emoji ⚡ + ⎇ main
  - Summary branch ├── N lanes · M active (dim metadata)
  - State sections (RUNNING/READY/CONFLICT/FAILED/LANDED) colored
  - Leaves on the grid: name (28) + rail (14) + meta (12) + age (6)
  - Footer: 3 hotkeys + 2 health indicators (daemon + active count)
  - Empty state: 💡 tip + numbered command suggestions

Tests: 21/21 passing in both Unicode and ASCII modes. ASCII assertions
verify tree connectors render and no Unicode bleeds through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter 2 months ago
parent
commit
8e218d907c
3 changed files with 1511 additions and 281 deletions
  1. 1065 193
      docs/DESIGN.md
  2. 400 49
      skills/_lib/term.sh
  3. 46 39
      skills/fleet-ops/scripts/fleet.sh

+ 1065 - 193
docs/DESIGN.md

@@ -1,281 +1,1153 @@
-# Terminal Output Design Language
+# Terminal Panel Design System
 
-> Status: **experimental**. The first skill on this language is `fleet-ops`.
-> New output-heavy skills should follow this guide and source `skills/_lib/term.sh`.
+> **Status:** Experimental. First consumer: `fleet-ops`.
+>
+> **Format:** Adapted from [google-labs-code/design.md](https://github.com/google-labs-code/design.md) — a structured design-spec template — and remapped to bash CLIs. Where that spec talks about screens, components, and tokens, this one talks about panels, sections, and glyphs.
 
-claude-mods ships ~70 skills, many of which write to a TTY (`fleet-ops`,
-`git-ops`, `push-gate`, `sync`, ...). When you run five of them in one session
-and each rolled its own glyphs and dividers, the toolkit feels like five
-toolkits. This document is the forcing function: one palette, one helper
-library, one shape.
+---
 
-## Principles
+## 1. Vision
 
-1. **Readable first, structured second, decorative last.** A pipe-friendly
-   plaintext line beats a beautiful one nobody can grep.
-2. **ASCII fallback is mandatory.** Every Unicode glyph has an ASCII twin.
-   Honor `TERM_ASCII=1`, `LANG` without UTF-8, and `TERM=dumb`.
-3. **Respect the pipe.** No color into non-TTY stdout. Honor `NO_COLOR`.
-   `FORCE_COLOR=1` overrides for CI tooling that wants ANSI in logs.
-4. **One screen of output preferred.** A status command should fit in 24
-   lines on a default terminal. Long output earns its length.
-5. **80 columns is the ceiling.** Some users still split panes. Tables that
-   exceed it must wrap or truncate, not scroll horizontally.
-6. **Color is signal, not skin.** Never use color as the *only* differentiator.
-   Glyphs and labels carry the meaning; color amplifies.
+A unified terminal-output design language for bash-based CLIs in the claude-mods family. One panel grammar, one set of glyphs, one grid. Tools that follow it feel like instruments on the same workbench instead of seventy hand-rolled formats.
 
-## Glyph Palette
+The aspiration: outputs that read as **deliberate, bespoke, and quiet** — like a well-laid PCB. Every glyph in its place, nothing decorative, nothing shouting. When a user runs five tools in a session, the toolkit feels coherent.
 
-State icons. Use through `term_state_icon` when possible; the literals are
-listed for cross-reference.
+---
 
-| Meaning | Unicode | ASCII | Color   | Use for                          |
-| ------- | ------- | ----- | ------- | -------------------------------- |
-| pending | ⏳      | `[.]` | yellow  | running, queued, in-flight       |
-| ready   | ✅      | `[+]` | green   | passed, ready to land            |
-| done    | 🚀      | `[*]` | green   | merged, shipped, terminal good   |
-| failed  | ❌      | `[x]` | red     | tests failed, refused, blocked   |
-| warning | ⚠️      | `[!]` | yellow  | conflict, hygiene flag           |
-| hint    | 💡      | `[i]` | cyan    | suggestion, next-step pointer    |
+## 2. Principles
 
-> Don't introduce new state glyphs without adding them here and to
-> `term_state_icon`. Improvising glyphs is what got us here.
+1. **Information first, ornament last.** Decoration that doesn't carry meaning gets cut.
+2. **Strip color and the layout still works.** Color amplifies; it never carries the only signal.
+3. **ASCII fallback is mandatory.** Every Unicode glyph has a 1–3 char ASCII proxy registered alongside it.
+4. **Use the invisible grid, not lines, to align.** Whitespace between columns aligns rows. Long horizontal rules are clutter.
+5. **Tether to the left.** Primary content rides the left rail. Right-side elements are leaves or iconography, never floating UI.
+6. **Let elements breathe.** Blank `│` rows between sections are content. Density without breath is unreadable.
+7. **Pops of color are dopamine; everywhere is wallpaper.** One brand emoji in the header, two health indicators in the footer, color on state words. That's the budget.
+8. **Borders are continuous.** Top and bottom rules run uninterrupted from corner to terminator. Gaps break the panel's "wrap the interface" feel.
+9. **One style per diagram.** Pick rounded corners, stick with rounded corners. Don't mix box families.
+10. **Same width, taller height for emphasis** — never wider. Width consistency is what makes columns line up; height variation gives presence without breaking the grid.
+11. **Bespoke, not branded.** No ASCII art logos. No flashy gradients. The polish is in placement and restraint.
 
-## Box Drawing
+---
 
-Use sparingly — borders that wrap nothing waste lines.
+## 3. Foundations
 
-| Role        | Unicode      | ASCII   |
-| ----------- | ------------ | ------- |
-| horizontal  | `─`          | `-`     |
-| vertical    | `│`          | `\|`    |
-| corners     | `┌ ┐ └ ┘`    | `+`     |
-| connectors  | `├ ┤ ┬ ┴ ┼`  | `+`     |
-| tree branch | `├─ └─ │`    | `+- \`- \|` |
+### 3.1 Color tokens
 
-`term_header` and `term_divider` already pick the right glyph based on
-`TERM_ASCII_MODE`. Reach for them before drawing your own boxes.
+Color is signal, never the only signal. Disabled when stdout isn't a TTY or `NO_COLOR` is set; forced on with `FORCE_COLOR=1`.
 
-## Layouts
+| Token         | ANSI    | Use for                                                  |
+| ------------- | ------- | -------------------------------------------------------- |
+| `accent`      | cyan    | Brand chrome (panel rules, hotkey letters, header rule)  |
+| `pending`     | yellow  | RUNNING, CONFLICT, modified files, HEAD marker           |
+| `ok`          | green   | READY, LANDED, healthy daemon, landed commits            |
+| `alarm`       | red     | FAILED, blocked, conflicts, critical health              |
+| `warn`        | orange  | Warning alerts, the inline alert triangle                |
+| `tag`         | magenta | Untracked files (lazygit/magit convention)               |
+| `meta`        | dim     | Counts, ages, base branch, timestamps, dotted leaders    |
+| `default`     | fg      | Branch names, file paths — the content the user came for |
 
-The default layout is **rule + grouped tree**: a horizontal-line "app
-header" on top, then items grouped by state with tree connectors. Flat
-tables are reserved for one-row-per-thing data where grouping would just
-add noise.
+### 3.2 Glyph palette
 
-### Header rule (the "app header")
+Every glyph below is registered with an ASCII fallback in `term.sh`. Don't introduce new ones without registering them.
 
-Always present. Title in cyan, trailing meta in dim. The rule extends to
-terminal width so the header reads as the section's banner.
+#### Panel and tree connectors
+
+| Role              | Unicode | ASCII   | Notes                                   |
+| ----------------- | ------- | ------- | --------------------------------------- |
+| corner: panel TL  | `╭`     | `+`     | Rounded — for the outer panel only      |
+| corner: panel TR  | `╮`     | `+`     | Rounded                                 |
+| corner: panel BL  | `╰`     | `+`     | Rounded                                 |
+| corner: panel BR  | `╯`     | `+`     | Rounded                                 |
+| T-junction        | `├`     | `+`     | Section attachment point                |
+| L-corner          | `└`     | `` ` `` | Last leaf in a section                  |
+| horizontal        | `─`     | `-`     | Rule fill                               |
+| vertical          | `│`     | `\|`    | Panel left edge, section continuation   |
+
+#### Rail glyphs (commit-graph and pipeline beads)
+
+| Role               | Unicode | ASCII | Meaning                          |
+| ------------------ | ------- | ----- | -------------------------------- |
+| commit (landed)    | `●`     | `*`   | a commit on the rail             |
+| HEAD               | `◉`     | `@`   | tip of the lane                  |
+| conflict           | `⊗`     | `X`   | rebase / merge failure point     |
+| link               | `─`     | `-`   | rail segment between commits     |
+
+#### Pip-bar glyphs (progress / completion)
+
+| Role       | Unicode | ASCII |
+| ---------- | ------- | ----- |
+| pip filled | `▰`     | `#`   |
+| pip empty  | `▱`     | `-`   |
+
+Default width: **10 pips** = clean 10% increments. Override only when the data has a natural denominator that isn't a percentage (`5 of 7 stages` → 7 pips).
+
+#### Health indicators (small bullets, colored)
+
+| Role     | Unicode | ASCII   | Notes                                   |
+| -------- | ------- | ------- | --------------------------------------- |
+| healthy  | `•`     | `(+)`   | Green, slowly pulsing in live mode      |
+| pending  | `•`     | `(.)`   | Yellow                                  |
+| warning  | `•`     | `(!)`   | Orange                                  |
+| critical | `•`     | `(!!)`  | Red                                     |
+| busted   | `⬤`     | `(X)`   | LARGE grey, motionless — unmissable     |
+| unknown  | `•`     | `(?)`   | Dim                                     |
+
+`•` (BULLET, U+2022) is smaller than `●` and reads as a tidy dot when colored. `⬤` (BLACK LARGE CIRCLE, U+2B24) is intentionally bigger to make a busted state unmissable.
+
+#### The terminator dot
+
+`●` is reserved as the right-edge terminator on header and footer rules. **Never** used as an inline divider, decorator, or health indicator. One job, one place.
+
+#### Brand emoji registry
+
+| Tool   | Unicode | ASCII |
+| ------ | ------- | ----- |
+| fleet  | ⚡       | `[F]` |
+| forge  | 🔨       | `[B]` |
+| psql   | 🐘       | `[P]` |
+| watch  | 📡       | `[M]` |
+| deploy | 🚀       | `[D]` |
+| git    | 🌿       | `[G]` |
+
+#### Header indicators
+
+| Role            | Unicode | ASCII | Use for                            |
+| --------------- | ------- | ----- | ---------------------------------- |
+| branch          | `⎇`     | `(b)` | `⎇ main` — base branch indicator   |
+
+#### Inline alert
+
+| Role     | Unicode | ASCII | Color  |
+| -------- | ------- | ----- | ------ |
+| warning  | `▲`     | `!`   | orange |
+| critical | `▲`     | `!`   | red    |
+
+#### Empty state
+
+| Role | Unicode | ASCII |
+| ---- | ------- | ----- |
+| tip  | `💡`     | `(i)` |
+
+#### Spinners (live mode only)
+
+Three families, each with a different role:
+
+**Working** — task actively progressing. Fast, 10 frames, ~80ms/frame.
+```
+⠋  ⠙  ⠹  ⠸  ⠼  ⠴  ⠦  ⠧  ⠇  ⠏
+```
+ASCII fallback: `|  /  -  \` (classic 4-frame).
+
+**Heartbeat** — daemon proof-of-life. Slow, 6 frames, ~600ms/cycle.
+```
+·  ∙  •  ●  •  ∙
+```
+ASCII fallback: `.  :  *  :`. Used in the footer health-indicator slot. Stops and goes grey when the daemon is busted.
+
+### 3.3 Spacing & the invisible grid
+
+Layout is built on whitespace alignment, not vertical bars. The grid for a leaf row in a panel:
+
+```
+[panel-vert] [section-indent] [tree-conn] [name-col]  [rail-col]    [meta-col]   [age-col]
+     │            ····             ├──     32 chars    14 chars     12 chars     6 chars
+```
+
+- **Panel vertical** — column 0, the panel's `│`.
+- **Section indent** — 4 cols of breathing room inside the panel.
+- **Tree connector** — `├──` or `└──` (4 cols including trailing space).
+- **Name column** — 32 cols, ellipsis-truncated past that (`feat/oauth-pkce-with-very-long…`).
+- **Rail column** — 14 cols, right-padded with spaces to align the next column.
+- **Meta column** — 12 cols (e.g., `M4 ?1`, `clean`, `blocked`).
+- **Age column** — 6 cols, right-aligned.
+
+These widths target an **80-col default**. They scale: a `--wide=120` mode bumps name to 48 and rail to 20. They never exceed terminal width — at <60 cols, drop the rail and meta columns rather than wrap.
+
+Section rows ride the same indent: `│   ` (panel + 3 spaces) to land at the section-indent column.
+
+---
+
+## 4. Components
+
+### 4.1 Panel
+
+The outer frame: header bar, body, footer bar. The body is wrapped by the panel's `│` running unbroken from `╭──` down to `╰──`.
+
+```
+╭── ⚡ fleet ─────────────────────────────────  ⎇ main ───●
+│
+[body]
+│
+╰── R refresh · L land · ? help ───── • daemon  • 17m ───●
+```
+
+**Rules**
+- Top rule starts at column 0 with `╭──`, ends at the right with terminator `●`.
+- Bottom rule mirrors with `╰──` and a terminator.
+- The rules have no whitespace gaps. `─` fills every span between elements.
+- Body lives between the rules; every body line begins with `│`.
+
+**Helper:** `term_panel_open` / `term_panel_close`.
+
+### 4.2 Header bar
+
+```
+╭── ⚡ fleet ─────────────────────────────────  ⎇ main ───●
+   └┬─┘ └─┬─┘                                  └──┬──┘ └┬┘
+    │    │                                        │     └─ terminator
+    │    └─ tool name (cyan)                      └─ right indicator (≤ 1)
+    └─ brand emoji (always before name)
+```
+
+**Rules**
+- **Brand emoji + tool name** at top-left, in that order, always. The emoji *is* the tool's identity at a glance.
+- **One indicator** at top-right max — typically a context tag (`⎇ main`, `db: production`, `region: us-east`). Format: `<icon> <value>` or `key: value` in dim.
+- The rule (`─`) fills every gap between brand and indicator and indicator and terminator.
+
+**Helper:** `term_panel_open <emoji_key> <name> <indicator>`.
+
+### 4.3 Footer bar
+
+```
+╰── R refresh · L land · ? help ───── • daemon  • 17m ───●
+   └─────────┬──────────┘             └────┬─────┘     └┬┘
+             │                             │            └─ terminator
+             │                             └─ health indicators (≤ 2)
+             └─ hotkeys (≤ 3)
+```
+
+**Rules**
+- **Up to 3 hotkeys** at bottom-left, format `<key> <verb>`, separated by `·`. Hotkey letters in cyan.
+- **Up to 2 health indicators** at bottom-right, format `• <text>`. **Two spaces** between indicators (no `·` separator — `•` is already a strong leading marker).
+- Continuous rule `─` fills the gap between hotkeys and health.
+- `●` terminator at far right.
+
+**Helper:** `term_panel_close <hotkeys> <healths>`.
+
+### 4.4 Section
+
+A grouped block under the header. Section labels are colored by state; no glyph at the junction, no trailing rule.
+
+```
+├── RUNNING (2)
+│   ├── feat/oauth-pkce       ●─●─●─◉      M4 ?1      12m
+│   └── spike/wasm-eval       ●─●─●─●─◉    M7         34m
+│
+├── READY (2)
+│   ├── fix/cache-bust        ●─◉          clean       2m
+│   └── chore/bump-axios      ●─◉          clean       5m
+```
+
+**Rules**
+- Section header: `├── LABEL (count)`, label colored by state.
+- No icon at the junction. State is carried by the **label color** plus the **label text**.
+- One blank `│` row of breath between sections — never zero, never two.
+- Empty sections are omitted; never render `(0)`.
+
+**Helper:** `term_section <state> <label> <count>`.
+
+### 4.5 Summary line
+
+A metadata-only branch of the panel. Tethers to the left rail like a section but renders in dim because it's reference, not actionable.
+
+```
+├── 4 lanes · 3 active
+```
+
+**Rules**
+- Same `├──` connector as a section.
+- No count in parens (it's a label, not a bucket).
+- Rendered dim throughout so it visually recedes below the colored state sections.
+- One blank `│` row above and below.
+
+### 4.6 Toast row
+
+A transient announcement at the top of the body, just under the header rule.
 
 ```
-── fleet ─────────────────────────────────────────────────────  4 lanes · 3 active
+╭── ⚡ fleet ──────────────────────────────────  ⎇ main ───●
+│
+├── ⚡ feat/oauth-pkce just LANDED              ← toast: dim cyan, fades
+│
+├── 4 lanes · 3 active
 ```
 
-### Grouped tree (default body)
+**Rules**
+- **At most one toast** at a time. Older toasts get replaced, not stacked.
+- Brand emoji leads the toast — reinforces "this is fleet news."
+- Color: dim cyan on the leading emoji, default fg on the message.
+- Lifetime: until next render in static mode; ~3s in live mode.
 
-**Tree-control rule:** the connectors `├─ │ └─` are the scaffold.
-**Nothing sits at a junction.** A junction is the point where a node's
-connector meets its parent's vertical — putting a glyph there breaks
-the eye-line that gives the tree its meaning.
+### 4.7 Inline alert
 
-- Group headers (interior nodes — they have children below) get **no
-  icon**. State is carried by the label text plus color.
-- Leaves (terminal nodes — nothing continues below them) **may** carry
-  an icon, since there's no vertical line to interrupt.
-- If you find yourself wanting an icon on an interior node, ask whether
-  it's really a group or just a decorated leaf — the answer is usually
-  the latter.
+A sub-row attached under a leaf, drawing attention without disrupting structure.
 
-#### 2-level: groups → leaves
+```
+│   ├── feat/audit-log        ●─●─⊗        blocked    17m
+│   │   ▲ rebase against main failed at 4ff21e6
+│   └── feat/oauth-pkce       ●─●─●─◉      M4 ?1     12m
+```
+
+**Rules**
+- Sub-row only — never replaces the leaf headline.
+- `▲` triangle leads the message, colored by severity:
+  - orange = warning, recoverable
+  - red = critical, blocks progress
+- Indented under the leaf's `│` continuation (column 8 in the standard grid).
+- ASCII fallback: `!`.
+
+### 4.8 Leaf
+
+A single row inside a section. The atomic unit of content.
+
+```
+│   ├── feat/oauth-pkce       ●─●─●─◉      M4 ?1      12m
+    └┬─┘ └────────┬────────┘  └───┬───┘    └──┬──┘    └┬┘
+     │            │               │           │        └─ age (right-aligned)
+     │            │               │           └─ meta (file-status shorthand)
+     │            │               └─ leaf glyph (one style only)
+     │            └─ name (ellipsis-truncate at column boundary)
+     └─ tree connector (├── except last sibling = └──)
+```
+
+**Rules**
+- **Choose one leaf glyph style per panel.** Rail (`●─●─◉`) for git-style data. Pip bar (`▰▰▰▱`) for percentage-style. Don't mix in the same panel.
+- **All columns conform to the grid.** The rail/pip column is fixed-width and right-padded; meta and age land in their own columns.
+- **Health/icon indicator on a leaf goes at the START** of the row, before the name — only when the indicator is *useful* on a per-leaf basis (typically only in flat / ungrouped views — in grouped views the section already conveys state).
+- **Long names ellipsis-truncate** at the name column boundary: `feat/oauth-pkce-with-very-long…`. Don't word-wrap in the body — wrapping breaks the grid.
 
-The default for state-bucketed views (lanes, PR checks, jobs). Group
-labels read as plain text, the `│` runs unbroken down column 0.
+**Helper:** `term_leaf <name> <rail_or_pips> <meta> <age>`.
+
+### 4.9 Rail (commit / pipeline graph)
 
 ```
-── fleet ─────────────────────────────────────────────────────  4 lanes · 3 active
-├─  RUNNING     (2)
-│  ├─ feat/auth-rewrite             12m
-│  └─ spike/wasm-eval               34m
-├─  READY       (1)
-│  └─ fix/cache-bust                2m
-└─  LANDED      (1)
-   └─ chore/bump-deps               1h
+●─●─●─◉      a 3-commit lane with HEAD
+●─●─●─●─◉    a 4-commit lane
+●─◉          1 commit ahead
+●─●─⊗        conflict at the third commit
+─            empty rail (queued, no commits yet)
 ```
 
-The double space after each connector (`├─ ` + leading space on the
-label) gives the eye a small breath before the label, reinforcing that
-the connector is structural and the label is content.
+**Rules**
+- Use only on leaves whose data is naturally a chain (commits, pipeline stages).
+- Right-pad to the rail column width so subsequent columns align.
+- HEAD marker (`◉`) is always last; conflict marker (`⊗`) replaces HEAD at the failure point.
+
+**Helper:** `term_rail <commits_ahead> <head_state>`.
+
+### 4.10 Pip bar (progress / completion)
+
+#### Anatomy
+
+```
+▰▰▰▱▱▱▱▱▱▱   30%
+└─┘└──────┘
+filled  empty
+state-color  dim
+```
+
+- **Default width: 10 pips.** Clean 10% increments, easy mental math.
+- **Override only for natural denominators** that aren't percentage (`5 of 7 stages` → 7 pips).
+- **Filled pip color** = state color (depends on metric type, see below).
+- **Empty pip color** = dim grey, always.
+
+#### Color by metric type
 
-Why grouped instead of flat: when ten lanes are in flight, scanning a
-flat table for "what's actually ready to land?" forces your eyes to do
-the filtering. Grouping does it for you, and the count tells you at a
-glance whether the answer is none, one, or twelve.
+The filled-pip color depends on what the metric *means*. Three families:
 
-#### 3-level: groups → branches → leaves
+**A. Progress** — work in motion, more = closer to done.
+```
+▰▰▰▱▱▱▱▱▱▱   30%  running build         yellow  (in-flight)
+▰▰▰▰▰▰▰▰▰▰  100%  build done            green   (terminal good)
+```
 
-For hierarchies with intermediate structure — repos with branches with
-files, projects with packages with tests, lanes with commits with
-patches. Interior nodes (`main`, `src/`, `utils/`) stay icon-free; only
-the leaves carry glyphs (state of the file).
+**B. Score / pass rate** — static measurement, more = better.
+```
+▰▰▱▱▱▱▱▱▱▱   20%  test pass rate        red     (alarm)
+▰▰▰▰▰▱▱▱▱▱   50%  test pass rate        yellow  (warn)
+▰▰▰▰▰▰▰▰▱▱   80%  test pass rate        green   (ok)
+```
+Thresholds: <33% red, <66% yellow, ≥66% green. Override per-skill.
 
+**C. Capacity / load** — utilization, more = worse (heading toward limits).
 ```
-── repo ──────────────────────────────────────────────────────  X:/Forge/claude-mods · 2 worktrees
-├─  main
-│  ├─  src/
-│  │  ├─ index.ts                   ⚠️  modified
-│  │  └─  utils/
-│  │     ├─ format.ts               ⚠️  modified
-│  │     └─ parse.ts                ✅  added
-│  └─ README.md                     clean
-└─  feat/auth-rewrite
-   └─  src/
-      ├─ auth.ts                    ✅  new
-      └─  middleware/
-         └─ session.ts              ⚠️  modified
+▰▰▱▱▱▱▱▱▱▱   20%  disk used             green   (plenty of room)
+▰▰▰▰▰▰▰▱▱▱   70%  disk used             yellow  (warn)
+▰▰▰▰▰▰▰▰▰▱   90%  disk used             red     (alarm)
 ```
+Thresholds: <60% green, <80% yellow, ≥80% red. Override per-skill.
 
-Look at any `├─` or `└─` and trace upward: there's always a clean `│`
-or empty space directly above it, never a glyph. That's the rule.
+**Helpers:**
+```bash
+term_pip_bar progress  n total          # type A
+term_pip_bar score     n total          # type B
+term_pip_bar capacity  n total          # type C
+```
 
-Each level adds a 3-column indent: `│  ` while the ancestor still has
-siblings to render, `   ` once the ancestor is on its last sibling. The
-helpers in `term.sh` (`term_tree_node`, `term_tree_indent`,
-`term_tree_connector`) compose this prefix so you don't have to count
-spaces.
+The metric type drives color selection. Skills don't pick a color directly — they pick the *kind* of measurement and the helper does the right thing.
 
-### Flat status table (escape hatch)
+### 4.11 Health indicator
 
-When the data is genuinely flat — `git status`-style fields, a single
-PR's checks — drop the tree. Glyph-first, no nested tables.
+A colored bullet followed by descriptive text. Lives in the footer's bottom-right slot (≤ 2 per panel) or at the start of a leaf when needed.
 
 ```
-── push-gate ─────────────────────────────────────────────────  refusing
-  ✅  secret scan        clean
-  ✅  forbidden files    none
-  ❌  divergence         3 ahead, 1 behind
+• daemon         healthy   (green pulsing)
+• 17m idle       slow / pending  (yellow)
+• lagging        warning  (orange)
+• down           critical  (red)
+⬤ daemon         busted    (large grey, static)
 ```
 
-The header rule still anchors the section; only the body is flat.
+**Rules**
+- `•` (small bullet) leads the text, single space between.
+- `⬤` (large) replaces `•` only when the system is *busted* — visually unmissable.
+- Text is short — 1–3 words max.
+- Color of the bullet must match the semantic meaning.
+- Two spaces between consecutive indicators — no `·` separator (the bullet is a strong enough lead).
 
-### Section divider
+**Helper:** `term_health <state> <text>`.
 
-Plain rule between blocks. No title.
+### 4.12 Hotkey hint
 
 ```
-────────────────────────────────────────────────────────────────
+R refresh
+L land
+? help
 ```
 
-### Empty state
+**Rules**
+- Single key (or modifier+key like `^C`) followed by a verb.
+- Letters in cyan.
+- Up to 3 in the footer; dropdown to a `?` help screen if more are needed.
+- Separated by `·` in the rendered footer (the dot disambiguates adjacent letter+verb pairs).
+
+**Helper:** `term_hotkey <key> <verb>`.
 
-Dim, parenthesised, single line — never a multi-line "nothing here" banner.
+### 4.13 Count
+
+Always `(n)`. Bare numbers belong in prose; counts in `()` belong in chrome.
 
 ```
-  (no lanes — run: fleet init <name>...)
+RUNNING (2)
+4 lanes · 3 active        <- prose, no parens
 ```
 
-## Colors
+### 4.14 Spinner
+
+Live-mode component. Replaces a single glyph in place as it cycles through frames.
 
-Color is signal layered on top of glyph and label. Strip color and the
-output must still be readable.
+- **Working** spinner cycles `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏` at ~80ms/frame on rows whose work is in progress (replaces the leaf glyph during the operation).
+- **Heartbeat** spinner cycles `· ∙ • ● • ∙` at ~600ms/cycle on the daemon health indicator. Stops and turns into a static grey `⬤` when the daemon is busted.
 
-| Color  | Meaning                                  |
-| ------ | ---------------------------------------- |
-| green  | success, terminal-good (READY, LANDED)   |
-| yellow | pending or warning (RUNNING, CONFLICT)   |
-| red    | failure (FAILED, refused)                |
-| cyan   | section headers, hints                   |
-| dim    | metadata: timestamps, counts, hint text  |
+**Helper:** `term_spinner_frame working|heartbeat tick` returns the glyph for tick `n`.
 
-Disabled when stdout isn't a TTY, or `NO_COLOR` is set. Forced on with
-`FORCE_COLOR=1`.
+---
 
-## Examples (rendered)
+## 5. Patterns
 
-### Before — `fleet-ops` rolling its own (flat table, double rules)
+### 5.1 Grouped tree (default)
+
+The default for state-bucketed data. One panel, summary line, then state-grouped sections, each with leaves.
 
 ```
-── Fleet ──────────────────────────────────────────────────────
-        BRANCH                           STATUS     AGE
-────────────────────────────────────────────────────────────────
-  ⏳   feat/auth-rewrite                 RUNNING    12m
-  ✅   fix/cache-bust                    READY      2m
-  🚀   chore/bump-deps                   LANDED     1h
-────────────────────────────────────────────────────────────────
+╭── ⚡ fleet ─────────────────────────────────  ⎇ main ───●
+│
+├── 4 lanes · 3 active
+│
+├── RUNNING (2)
+│   ├── feat/oauth-pkce       ●─●─●─◉      M4 ?1      12m
+│   └── spike/wasm-eval       ●─●─●─●─◉    M7         34m
+│
+├── READY (2)
+│   ├── fix/cache-bust        ●─◉          clean       2m
+│   └── chore/bump-axios      ●─◉          clean       5m
+│
+├── CONFLICT (1)
+│   └── feat/audit-log        ●─●─⊗        blocked    17m
+│
+╰── R refresh · L land · ? help ───── • daemon  • 17m ───●
 ```
 
-### After — rule on top, grouped tree with unbroken connectors
+**When to use:** state matters more than time. Most CLIs.
+
+### 5.2 Flat rail (alternate)
+
+Same atoms, no grouping. Sorted by age. State moves to a per-leaf indicator at the start.
 
 ```
-── fleet ─────────────────────────────────────────────────────  3 lanes · 2 active
-├─  RUNNING     (1)
-│  └─ feat/auth-rewrite             12m
-├─  READY       (1)
-│  └─ fix/cache-bust                2m
-└─  LANDED      (1)
-   └─ chore/bump-deps               1h
+╭── ⚡ fleet ─────────────────────────────────  ⎇ main ───●
+│
+├── 4 lanes · 3 active
+│
+│   • fix/cache-bust         ●─◉          clean       2m
+│   • chore/bump-axios       ●─◉          clean       5m
+│   • feat/oauth-pkce        ●─●─●─◉      M4 ?1      12m
+│   • feat/audit-log         ●─●─⊗        blocked    17m
+│   • spike/wasm-eval        ●─●─●─●─◉    M7         34m
+│
+╰── R refresh · L land · ? help ───── • daemon  • 17m ───●
 ```
 
-The header rule stays — strongest cue you're inside a skill's output.
-Group labels are icon-free so the `│` running down column 0 is unbroken
-from the first group to the last leaf. State is carried by the label
-text and color (yellow for RUNNING, green for READY/LANDED). The tree
-reads as a tree, not a list with decorations.
+**When to use:** chronological or activity-sorted views (`fleet --flat`).
 
-### `git-ops/status` reformatted in the same language
+### 5.3 Status panel (no tree)
 
+For genuinely flat data — a PR's checks, a service's health summary. No sections, no tree connectors, just leaves under the panel `│`.
+
+```
+╭── ⚡ push-gate ───────────────────────────  refusing ───●
+│
+│   ✓  secret scan
+│   ✓  forbidden files
+│   ✗  divergence              3 ahead, 1 behind
+│
+╰── R retry · ? help ─────────────── • blocking ───●
 ```
-── Repo ───────────────────────────────────────────────────────  X:/Forge/claude-mods
-  branch    claude/sleepy-johnson-74f19d
-  HEAD      367b062 fix(skills/fleet-ops): consistent .claude/ path (2h ago)
-  sync      0 ahead / 0 behind
-  tree      0 staged / 2 unstaged / 1 untracked
 
-  ⚠️   HYGIENE  main checkout on 'claude/...' — feature work belongs in worktrees
+### 5.4 Multi-panel stacking
+
+For dashboards — fleet next to git status, build output next to test output. Side-by-side is **not** supported (column arithmetic doesn't survive); panels stack vertically.
+
 ```
+╭── ⚡ fleet ───────────────────────────  ⎇ main ───●
+│
+├── RUNNING (2)
+│   └── feat/oauth-pkce      ●─●─●─◉    12m
+│
+╰── R · L · ? ──── • daemon ───●
 
-### `push-gate` refusal
 
+╭── 🌿 git ─────────────────────────────  ⎇ main ───●
+│
+├── HEAD (3 ahead, 0 behind)
+│   └── 367b062  fix(skills): consistent path  ●─◉  2h
+│
+╰── s · c · ? ──── • clean ───●
 ```
-── push-gate ──────────────────────────────────────────────────  refusing
-  ❌   secret scan        2 hits in src/config/keys.ts
-  ✅   forbidden files    none
-  ✅   divergence         clean
 
-  💡   run: gitleaks detect --source . --no-git
+**Rule:** exactly **2 blank lines** between stacked panels. The terminator `●` and the opening `╭──` get to breathe; less than that they cling, more than that they drift apart.
+
+### 5.5 Help screen
+
+The `?` hotkey in every footer leads here. Same panel grammar, different content shape.
+
+```
+╭── ⚡ fleet · help ────────────────────────────  ⎇ main ───●
+│
+├── commands
+│   ├── R refresh     re-read lane state from disk
+│   ├── L land        merge a READY lane into base
+│   ├── F flat        switch to rail view, sorted by age
+│   └── ? help        you are here
+│
+├── concepts
+│   ├── lane          a branch managed by fleet
+│   ├── base          the trunk lanes merge into (default: main)
+│   └── daemon        background poller; auto-lands READY lanes
+│
+╰── q quit ─────────────────────── • v2.4.9 ───●
+```
+
+The header gets `· help` after the tool name. Title is contextual; everything else is the same panel.
+
+### 5.6 Empty state
+
+A whole panel when there's nothing to show. Empty states earn extra whitespace and become tutorials.
+
+```
+╭── ⚡ fleet ──────────────────────────────────  ⎇ main ───●
+│
+│
+│   no lanes yet
+│
+│
+│   💡 to get started:
+│
+│      1. fleet init feat/foo bar      create branches + worktrees
+│      2. (work in each lane)          commits, tests
+│      3. fleet start                  run the daemon
+│
+│
+╰── ? help ─────────────────────────── • v2.4.9 ───●
+```
+
+**Rules**
+- Empty state body uses **flat indented content** (no leaf tree connectors). Empty states are welcome posters, not data trees, so they get to break the tree convention.
+- The trunk `│` still tethers everything left.
+- 💡 emoji leads the "to get started" tip line.
+- 2 blank `│` rows above and below each block (vs. 1 in non-empty panels).
+- Footer drops to a single hotkey (`? help`) and a single status indicator (typically version).
+
+---
+
+## 6. Edge cases
+
+### 6.1 Long titles / names
+
+Ellipsis-truncate at the name-column boundary. Don't word-wrap in the body.
+
+```
+│   ├── feat/oauth-pkce-with-very-l…  ●─●─●─◉      M4 ?1      12m
+│   └── chore/bump-axios               ●─◉          clean       5m
 ```
 
-## Anti-patterns
+The full name is recoverable via `--wide` or the verbose view. Truncation preserves the grid.
+
+### 6.2 Narrow terminals (<60 cols)
+
+Drop columns from the right, in order: age → meta → rail. The leaf collapses to:
+
+```
+│   ├── feat/oauth-pkce
+│   └── spike/wasm-eval
+```
+
+Never wrap. Wrapping breaks the grid; truncation just hides data.
+
+### 6.3 ASCII fallback (`TERM_ASCII=1`, non-UTF locale, `TERM=dumb`)
+
+Every glyph has a registered ASCII proxy:
+
+```
++-- [F] fleet -------------------------------- (b) main ---*
+|
++-- 4 lanes · 3 active
+|
++-- RUNNING (2)
+|   +-- feat/oauth-pkce       *-*-*-@      M4 ?1      12m
+|   `-- spike/wasm-eval       *-*-*-*-@    M7         34m
+|
++-- READY (2)
+|   +-- fix/cache-bust        *-@          clean       2m
+|   `-- chore/bump-axios      *-@          clean       5m
+|
++-- CONFLICT (1)
+|   `-- feat/audit-log        *-*-X        blocked    17m
+|
+`-- R refresh · L land · ? help ---- (+) daemon  (.) 17m ---*
+```
+
+Same skeleton. Rounded corners (`╭ ╰`) collapse to `+`. Rail dots (`● ◉ ⊗`) become `* @ X`. Health bullets (`•`) become `(+) (.) (!)`. The grid survives.
+
+### 6.4 NO_COLOR
+
+Strip every ANSI sequence. The structure (glyphs, grid, indentation) carries 100% of the information. Verify: `NO_COLOR=1 fleet` should be unambiguous.
+
+### 6.5 Rendering context
+
+Panels and diagrams render only in monospace contexts with verbatim whitespace. There are exactly two:
+
+- **TTY output** — automatic. The canonical target.
+- **Fenced code blocks in any markdown** — ideal. Locks monospace, preserves the grid pixel-for-pixel. Use this in README, CHANGELOG, design docs, GitHub issues, anywhere markdown is rendered.
+
+Never paste unfenced. Markdown's `|` is table syntax; box-drawing collapses; whitespace compresses; the panel renders as visual nonsense. If a panel needs to live in prose, fence it.
+
+---
 
-- **Decorative emoji.** ✨📦🎉 carry no state. Keep the glyph budget for the
-  six in the palette.
-- **Nested tables or boxes.** A table inside a bordered box is two layouts
-  fighting for the same line. Pick one.
-- **Color as the only difference.** "Red row vs green row" fails for
-  CI logs, screen readers, and color-blind users. Always pair with a glyph.
-- **Lines past 80 columns by default.** If you genuinely need 120, gate it
-  behind `--wide` or auto-detect via `tput cols`.
-- **Assuming color in CI.** GitHub Actions sets `TERM=dumb`. Check.
-- **Multi-line empty states.** `(no lanes)` beats a 4-line ASCII shrug.
-- **New glyphs.** If your state doesn't fit pending/ready/done/failed/warn/hint,
-  the state probably collapses into one of them. If it really doesn't,
-  amend this document first.
+## 7. Anti-patterns
 
-## The library
+- **Long horizontal rules in the body** (`──────────── 🟡`). Decorative clutter; use whitespace to separate sections instead.
+- **Glyphs at tree junctions.** A glyph between `├──` and the parent's `│` breaks the eye-line of the tree.
+- **Mixing leaf glyph styles in one panel.** Rail and pips together looks like two languages fighting.
+- **Floating right-side UI.** Anything important tethers to the left rail. Right side is for leaves and small iconography (terminator dot, base-branch tag) only.
+- **More than one brand emoji per panel.** ⚡ in the header earns its keep. ⚡ next to every "running" lane is wallpaper.
+- **Using `●` as decoration.** Reserved for header/footer terminators. If you need a small marker elsewhere, use `•` (bullet, smaller) or pick a different shape (`◉ ◐ ◇ ▰ ⬢`).
+- **Bare numbers in chrome.** `RUNNING 2` is prose. `RUNNING (2)` is a count. Counts in chrome wear parens.
+- **Word-wrapping leaf rows.** Breaks the grid. Truncate with `…` instead.
+- **Section headers with `(0)`.** Empty sections are omitted, not rendered.
+- **Color-only state differentiation.** Red row vs green row fails on `NO_COLOR`, screen readers, and printouts. Always pair color with text or shape.
+- **Wider boxes for emphasis in diagrams.** Breaks column alignment. Use taller, same width.
+- **Multiple corner families in one diagram.** Pick rounded, stick with rounded.
 
-`skills/_lib/term.sh` is the single source of truth for glyphs, colors,
-and layout helpers. Source it, call `term_init`, then use:
+---
+
+## 8. Reference example
+
+```
+╭── ⚡ fleet ─────────────────────────────────  ⎇ main ───●
+│
+├── 4 lanes · 3 active
+│
+├── RUNNING (2)
+│   ├── feat/oauth-pkce       ●─●─●─◉      M4 ?1      12m
+│   └── spike/wasm-eval       ●─●─●─●─◉    M7         34m
+│
+├── READY (2)
+│   ├── fix/cache-bust        ●─◉          clean       2m
+│   └── chore/bump-axios      ●─◉          clean       5m
+│
+├── CONFLICT (1)
+│   └── feat/audit-log        ●─●─⊗        blocked    17m
+│
+╰── R refresh · L land · ? help ───── • daemon  • 17m ───●
+```
+
+Color map:
+- `╭── ─ ╰──` panel chrome: cyan (accent)
+- `⚡` brand emoji: as-is (yellow rendering)
+- `fleet`: cyan
+- `⎇ main`: dim
+- `4 lanes · 3 active`: dim
+- `├── │ └──`: dim cyan (recede)
+- `RUNNING`, `CONFLICT`: yellow (pending)
+- `READY`: green (ok)
+- `(2)`: dim
+- branch names: default fg
+- `●─●─●` (landed): green; `◉` (HEAD): yellow; `⊗` (conflict): red
+- `M4`: yellow; `?1`: magenta; `clean`: dim green; `blocked`: red
+- `12m`: dim
+- `R`, `L`, `?`: bright cyan; verbs: default
+- `•` (healthy): green, pulsing in live mode
+- `●` terminators: cyan
+
+---
+
+## 9. Implementation — `skills/_lib/term.sh`
+
+The library is the single source of truth. Skills source it; nothing else needs to know about glyphs or colors.
 
 ```bash
 LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" && pwd)"
 . "$LIB/term.sh"
 term_init
+```
+
+### Helpers
+
+```bash
+# Foundations
+term_init                                # detect TTY, NO_COLOR, TERM_ASCII, set globals
+term_color name "text"                   # green/yellow/red/cyan/dim/orange/magenta wrap
+term_emoji key                           # registered glyph + ASCII fallback
+
+# Components
+term_panel_open  emoji_key name [right_indicator]
+term_panel_close [hotkeys] [health_indicators]
+term_summary_line "text"                 # "├── 4 lanes · 3 active" (dim)
+term_section     state label count       # "├── LABEL (n)"
+term_leaf        name leaf_glyph meta age
+term_toast       emoji_key "text"        # toast row
+term_alert       severity "text"         # ▲ inline alert sub-row
+
+# Leaf glyph builders (pick one per panel)
+term_rail        commits_ahead head_state    # ●─●─●─◉ / ●─●─⊗
+term_pip_bar     metric_type filled total    # progress / score / capacity
+
+# Right-side furniture
+term_health      state text              # • daemon (colored, with ⬤ for busted)
+term_hotkey      key verb                # R refresh
+
+# Live mode
+term_spinner_frame family tick           # working / heartbeat → glyph
+
+# Edge cases
+term_truncate    "text" max_cols         # ellipsis-truncate
+term_term_width                          # current cols
+```
 
-term_header "Fleet" "$count lanes"
-term_table_row "$(term_state_icon READY)" "$branch" "READY" "$age"
-term_empty "no lanes — run: fleet init <name>..."
+### Registries
+
+Centralized at the top of `term.sh`, sourced into associative arrays:
+
+```bash
+declare -A TERM_BRAND=(
+  [fleet]="⚡|[F]"
+  [forge]="🔨|[B]"
+  [psql]="🐘|[P]"
+  [watch]="📡|[M]"
+  [deploy]="🚀|[D]"
+  [git]="🌿|[G]"
+)
+
+declare -A TERM_HEALTH=(
+  [healthy]="•|(+)"
+  [pending]="•|(.)"
+  [warning]="•|(!)"
+  [critical]="•|(!!)"
+  [busted]="⬤|(X)"
+  [unknown]="•|(?)"
+)
+
+declare -A TERM_DIAGRAM_ICON=(
+  [user]="👤|(U)"
+  [web]="🌐|(W)"
+  [mobile]="📱|(M)"
+  [auth]="🔐|(A)"
+  [database]="🗄|(D)"
+  [cache]="⚡|(C)"
+  [queue]="📨|(Q)"
+  [storage]="📦|(P)"
+  [service]="⚙|*"
+  [api]="🔌|(I)"
+  [search]="🔍|(S)"
+  [timer]="⏱|(T)"
+  [build]="🔨|(B)"
+  [hook]="🪝|(H)"
+  [log]="📄|(F)"
+)
 ```
 
-The helpers no-op gracefully under `NO_COLOR`, non-TTY, and `TERM_ASCII=1`.
-That's the whole contract — if you're reaching for raw `\033[` codes in a
-skill, you're off the path.
+Adding a tool means one row. Adding a state means one row. No hardcoded escape sequences in skills.
+
+---
+
+## 10. Open questions
+
+- **`--wide` mode.** Should claude-mods skills auto-detect `tput cols >= 120` and widen the grid, or always default to 80? Lean toward always-80 unless user opts in.
+- **Animation framework.** Spinners and live-updating panels (`fleet --watch`). The static-output spec is clear; a live mode would need a separate component family covering frame timing and partial redraws.
+- **Sub-panels.** A `verbose` view could nest a sub-panel per leaf. Not covered yet — defer until two skills genuinely need it.
+- **Accessibility audit.** Verify the panel reads coherently to screen readers when piped through `aspell` or similar. The "structure carries information" principle should hold; needs proof.
+
+---
+
+## 11. Diagrams
+
+The panel grammar handles lists and trees. For relationships — services talking to each other, state transitions, decision flows, timelines — diagrams take over. Same grid, same glyph palette, same color tokens; just different compositions.
+
+Diagrams render in code-fenced blocks (TTY or markdown); they are otherwise subject to the same rules as panels (§6.5). They may stand alone in docs or live inside a panel as body content.
+
+### 11.1 Foundations
+
+- **Grid**: cells are 1 char wide. Connections are orthogonal — horizontal `─` and vertical `│` only. Diagonals (`\` `/`) read poorly in monospace; don't use them.
+- **One corner family per diagram**: rounded `╭ ╮ ╰ ╯` is the canonical choice for diagrams in this system. Mix rounded with light corners (`┌ └`) only inside layered stacks (§11.5).
+- **Colors**: same tokens as panels. State words wear their state colors inside diagrams too.
+
+### 11.2 Box anatomy
+
+Every box in a diagram follows the same construction rules.
+
+#### Width
+
+`width = max(label_with_icon) + 4`
+
+The longest label on the page (icon + space + text) plus 4 cells of padding (3 left, 1 right). All boxes on the same diagram are this width — alignment is the price of admission.
+
+#### Height
+
+- **Standard**: 1 content row (3 lines total: top corner, label, bottom corner).
+- **Emphasis**: 3 content rows (5 lines total: top corner, blank, label, blank, bottom corner). Same width, taller height — never wider.
+
+#### Label position
+
+Top-anchored, right-aligned, **1-char right padding**.
+
+```
+╭──────────────╮
+│       🌐 web │     ← label hits "1-char-from-right"; left side absorbs slack
+╰──────────────╯
+```
+
+Left padding = `width − label_cells − 1 − 2` (corners). The constant 1-char right pad is what makes labels visibly right-anchored across boxes with mixed icon/no-icon content.
+
+Cell-width counting: emoji = 2 cells; standard glyphs and ASCII chars = 1 cell.
+
+#### Examples — same 16-wide box, varied content
+
+```
+╭──────────────╮     ╭──────────────╮     ╭──────────────╮
+│     👤 users │     │           lb │     │       🌐 web │
+╰──────────────╯     ╰──────────────╯     ╰──────────────╯
+
+╭──────────────╮     ╭──────────────╮     ╭──────────────╮
+│    📱 mobile │     │      🔐 auth │     │    🔍 search │
+╰──────────────╯     ╰──────────────╯     ╰──────────────╯
+
+╭──────────────╮     ╭──────────────╮     ╭──────────────╮
+│         user │     │       orders │     │          pay │
+╰──────────────╯     ╰──────────────╯     ╰──────────────╯
+```
+
+Every label's last character lands at column-from-right = 1, regardless of icon presence.
+
+#### Emphasis — same width, taller
+
+```
+╭──────────────╮          ╭──────────────╮
+│    🔌 api gw │          │              │
+╰──────────────╯          │    🔌 api gw │
+                          │              │
+   standard               ╰──────────────╯
+   3 lines total
+                            emphasis
+                            5 lines total
+```
+
+Use sparingly: at most 1 emphasis box per diagram. The point of emphasis is to draw the eye; multiple emphases scatter it.
+
+### 11.3 Connectors and arrows
+
+#### Straight connectors
+
+```
+──▶          horizontal right
+◀──          horizontal left
+▲            vertical up
+▼            vertical down
+```
+
+#### Bent connectors
+
+Use the rounded corner family:
+
+```
+──╮                   ╭──
+  │                   │
+  ▼                   ▼
+
+      ─╯           ╰─
+       │           │
+```
+
+#### Junctions (orthogonal multi-way)
+
+```
+─┬─    drop down from horizontal
+─┴─    rise up to horizontal
+─├─    branch right from vertical
+─┤─    branch left from vertical
+─┼─    cross
+```
+
+#### Arrowheads
+
+```
+Standard       ▶  ◀  ▲  ▼          filled triangle (default)
+Open           ▷  ◁  △  ▽          for "weak" / optional connections
+ASCII          >  <  ^  v
+```
+
+**Rule:** orthogonal lines only. If two boxes aren't on the same row, bend the connector with a corner; never use diagonals.
+
+### 11.4 Connector labels
+
+Labels go above the line for outgoing, below for incoming.
+
+```
+                req
+client ────────────▶ server
+       ◀────────────
+                resp
+```
+
+For vertical fan-outs, labels sit between the junction and the arrowhead:
+
+```
+       ╭─────────┴─────────╮
+       │                   │
+      no                  yes
+       │                   │
+       ▼                   ▼
+```
+
+### 11.5 Composed patterns
+
+#### Architecture (boxes + arrows)
+
+```
+╭──────────────╮      ╭──────────────╮      ╭──────────────╮
+│       🌐 web │ ───▶ │    🔌 api gw │ ───▶ │     🗄 pgsql │
+╰──────────────╯      ╰──────────────╯      ╰──────────────╯
+```
+
+#### Cluster (container holding boxes)
+
+The one place where mixing corner families is allowed: double `╔ ╝` for the container, rounded for interior nodes.
+
+```
+╔═ web tier ═════════════════════════╗
+║                                    ║
+║   ╭──────────────╮ ╭──────────────╮║
+║   │       🌐 web │ │    📱 mobile │║
+║   ╰──────────────╯ ╰──────────────╯║
+║                                    ║
+╚════════════════════════════════════╝
+```
+
+#### Decision flow
+
+```
+                    ╭──────────────╮
+                    │        start │
+                    ╰───────┬──────╯
+                            │
+                            ▼
+                    ╭──────────────╮
+                    │       ready? │
+                    ╰───────┬──────╯
+                            │
+                ╭───────────┴───────────╮
+                │                       │
+               no                      yes
+                │                       │
+                ▼                       ▼
+        ╭──────────────╮        ╭──────────────╮
+        │         wait │        │         land │
+        ╰──────────────╯        ╰──────────────╯
+```
+
+Fan-out goes through a `┴` junction below the diamond — keeps every box the same width.
+
+#### State machine
+
+```
+   ╭──────────────╮      ╭──────────────╮      ╭──────────────╮
+   │      RUNNING │ ───▶ │        READY │ ───▶ │       LANDED │
+   ╰───────┬──────╯      ╰───────┬──────╯      ╰──────────────╯
+           │                     │
+           ▼                     ▼
+   ╭──────────────╮      ╭──────────────╮
+   │       FAILED │      │     CONFLICT │
+   ╰──────────────╯      ╰──────────────╯
+```
+
+State labels in their state colors. Terminal states (LANDED, FAILED, CONFLICT) have no outgoing arrows.
+
+#### Sequence / lifeline
+
+```
+client                 server
+  │                      │
+  ├── login ────────────▶│
+  │                      │
+  │◀──── token ──────────┤
+  │                      │
+  ├── /api/data ────────▶│
+  │                      │
+  │◀──── 200 ok ─────────┤
+  │                      │
+```
+
+Lifelines as `│` columns, messages as `├──▶` arrows. Time runs top-down.
+
+#### Pipeline with status
+
+```
+   build              test              deploy
+╭──────────────╮  ╭──────────────╮  ╭──────────────╮
+│ ●  ●  ●      │  │ ●  ●  ◌      │  │ ◌  ◌  ◌      │
+╰──────────────╯  ╰──────────────╯  ╰──────────────╯
+   done              running           pending
+```
+
+Each box's interior shows work units as filled/empty dots. Pipeline reads as both flow and status.
+
+#### Hierarchy
+
+```
+                   ╭──────────────╮
+                   │         core │
+                   ╰───────┬──────╯
+                           │
+              ╭────────────┼────────────╮
+              │            │            │
+              ▼            ▼            ▼
+      ╭──────────────╮╭──────────────╮╭──────────────╮
+      │       🔐 auth││         data ││           ui │
+      ╰──────────────╯╰──────────────╯╰──────────────╯
+```
+
+Tree shape with proper boxes — for deps where the layout is the point.
+
+#### Layered stack (one allowed exception to rounded-only)
+
+```
+┌─ application ────────────────┐
+│   bash + term.sh             │
+├─ runtime ────────────────────┤
+│   git, stat, sed, awk        │
+├─ filesystem ─────────────────┤
+│   .claude/fleet/             │
+└──────────────────────────────┘
+```
+
+Light corners (`┌ ┐ └ ┘`) for stacks — distinguishes them from panels and diagrams. Layer name in the top edge of each layer.
+
+### 11.6 Icon dictionary
+
+A small registered set for diagrams. Use sparingly — at most one per box, only when the icon adds meaning. ASCII fallback registered for each.
+
+| Concept            | Glyph | ASCII | Use for                                  |
+| ------------------ | ----- | ----- | ---------------------------------------- |
+| user / actor       | 👤     | `(U)` | external person/role at the system edge  |
+| web / browser      | 🌐     | `(W)` | client web tier                          |
+| mobile             | 📱     | `(M)` | mobile clients                           |
+| auth / security    | 🔐     | `(A)` | auth services, key vaults                |
+| database           | 🗄     | `(D)` | persistent storage                       |
+| cache              | ⚡     | `(C)` | fast in-memory stores                    |
+| queue / message    | 📨     | `(Q)` | message brokers, event buses             |
+| storage / blob     | 📦     | `(P)` | object storage, file blobs               |
+| service / worker   | ⚙     | `*`   | background processes, scheduled jobs     |
+| api / endpoint     | 🔌     | `(I)` | api gateway, ingress                     |
+| search / index     | 🔍     | `(S)` | search services                          |
+| timer / schedule   | ⏱     | `(T)` | scheduled tasks, crons                   |
+| build / compile    | 🔨     | `(B)` | build systems                            |
+| event / hook       | 🪝     | `(H)` | webhooks, event triggers                 |
+| log / file         | 📄     | `(F)` | logs, files, records                     |
+
+**Rules for icons in diagrams:**
+1. **At most one icon per box.** No stacking.
+2. **Icon goes inside the box, before the label.** Same row, single space between.
+3. **Boxes must size for emoji width.** Emoji = 2 cells; size the box accordingly.
+4. **Be selective.** A diagram of 16 boxes shouldn't have 16 icons — pick 4–6 *categories* of node and icon those (one for each tier or role).
+
+### 11.7 Reference exemplar — 16-element architecture
+
+```
+                          ╭──────────────╮
+                          │     👤 users │
+                          ╰───────┬──────╯
+                                  │
+                                  ▼
+                          ╭──────────────╮
+                          │           lb │
+                          ╰───────┬──────╯
+                                  │
+                       ╭──────────┴──────────╮
+                       │                     │
+                       ▼                     ▼
+                 ╭──────────────╮     ╭──────────────╮
+                 │       🌐 web │     │    📱 mobile │
+                 ╰───────┬──────╯     ╰───────┬──────╯
+                         │                    │
+                         ╰─────────┬──────────╯
+                                   │
+                                   ▼
+                          ╭──────────────╮
+                          │    🔌 api gw │
+                          ╰───────┬──────╯
+                                  │
+        ╭──────┬──────────┬───────┼───────┬──────────┬──────╮
+        │      │          │       │       │          │      │
+        ▼      ▼          ▼       ▼       ▼          ▼      ▼
+   ╭──────────────╮ ╭──────────────╮ ╭──────────────╮ ╭──────────────╮ ╭──────────────╮
+   │      🔐 auth │ │         user │ │       orders │ │          pay │ │    🔍 search │
+   ╰───────┬──────╯ ╰───────┬──────╯ ╰───────┬──────╯ ╰───────┬──────╯ ╰───────┬──────╯
+           │                │                │                │                │
+           ▼                ▼                ▼                ▼                ▼
+   ╭──────────────╮ ╭──────────────╮ ╭──────────────╮ ╭──────────────╮ ╭──────────────╮
+   │     🗄 pgsql │ │     ⚡ redis │ │     📨 kafka │ │       stripe │ │    🗄 elastic│
+   ╰──────────────╯ ╰──────────────╯ ╰───────┬──────╯ ╰──────────────╯ ╰──────────────╯
+                                             │
+                                             ▼
+                                    ╭──────────────╮
+                                    │     ⚙ worker │
+                                    ╰──────────────╯
+```
+
+16 elements, all the same width, all rounded corners. Icons on tier-defining nodes (clients, gateway, security, search, data layer, worker); domain services (`user`, `orders`, `pay`, `lb`, `stripe`) stay icon-free where the name is the meaning.
+
+### 11.8 Rules of thumb
+
+- **Don't draw if you can list.** A bulleted list always wins for purely sequential content. Diagrams earn their keep when there's a *spatial* relationship the reader needs to grasp.
+- **Pick one corner family per diagram.** Rounded everywhere; the cluster container double-line and the layered-stack light-corner are the only sanctioned exceptions.
+- **Width budget: 80 cols.** Diagrams that need >100 cols need to be split or rethought.
+- **Color is amplification.** Strip color, the diagram still works.
+- **No diagonals, no overlapping lines, no crossing connectors.** If your diagram needs them, it's the wrong representation for monospace.
+- **Anti-pattern: ASCII art logos and decorative borders.** This is about communicating structure, not flexing.
+
+---
+
+## Appendix A: rules of skill-agent-updates
+
+Output-heavy skills follow this spec and source `skills/_lib/term.sh`. See [`rules/skill-agent-updates.md`](../rules/skill-agent-updates.md).

+ 400 - 49
skills/_lib/term.sh

@@ -1,25 +1,46 @@
 #!/usr/bin/env bash
-# term.sh — shared terminal-output helpers for claude-mods skills.
+# term.sh — terminal panel design system for claude-mods skills.
 #
 # Source from any skill script:
 #   LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" && pwd)"
 #   . "$LIB/term.sh"
 #   term_init
 #
-# Honors: NO_COLOR, FORCE_COLOR, TERM_ASCII=1.
+# Honors: NO_COLOR, FORCE_COLOR, TERM_ASCII=1, FLEET_ASCII=1 (legacy).
 # Status: experimental — see docs/DESIGN.md.
 
 # Guard against double-sourcing.
 [[ -n "${__TERM_SH_LOADED:-}" ]] && return 0
 __TERM_SH_LOADED=1
 
-# Globals populated by term_init.
+# ─── Globals (populated by term_init) ──────────────────────────────────────
 TERM_TTY=0
 TERM_COLOR=0
 TERM_ASCII_MODE=0
 TERM_WIDTH=80
 
-# State icons (set by term_init based on TERM_ASCII_MODE).
+# ─── ANSI escapes (empty when color disabled) ─────────────────────────────
+TERM_C_GREEN=""
+TERM_C_YELLOW=""
+TERM_C_ORANGE=""
+TERM_C_RED=""
+TERM_C_CYAN=""
+TERM_C_MAGENTA=""
+TERM_C_DIM=""
+TERM_C_OFF=""
+
+# ─── Tree connectors (set by term_init based on TERM_ASCII_MODE) ──────────
+TERM_TREE_BRANCH=""    # ├─  /  +-
+TERM_TREE_LAST=""      # └─  /  `-
+TERM_TREE_VERT=""      # │   /  |
+
+# ─── Panel chrome ─────────────────────────────────────────────────────────
+TERM_PANEL_TL=""       # ╭   /  +
+TERM_PANEL_BL=""       # ╰   /  +
+TERM_PANEL_HRULE=""    # ─   /  -
+TERM_PANEL_TERM=""     # ●   /  *
+
+# ─── Legacy state icons (kept for backwards-compat with fleet.sh) ─────────
 TERM_ICON_PENDING=""
 TERM_ICON_READY=""
 TERM_ICON_DONE=""
@@ -27,14 +48,57 @@ TERM_ICON_FAILED=""
 TERM_ICON_WARN=""
 TERM_ICON_HINT=""
 
-# ANSI escapes (empty when color disabled).
-TERM_C_GREEN=""
-TERM_C_YELLOW=""
-TERM_C_RED=""
-TERM_C_CYAN=""
-TERM_C_DIM=""
-TERM_C_OFF=""
+# ─── Registries (Unicode|ASCII) ───────────────────────────────────────────
+declare -A TERM_BRAND=(
+  [fleet]="⚡|[F]"
+  [forge]="🔨|[B]"
+  [psql]="🐘|[P]"
+  [watch]="📡|[M]"
+  [deploy]="🚀|[D]"
+  [git]="🌿|[G]"
+)
+
+declare -A TERM_HEALTH_GLYPH=(
+  [healthy]="•|(+)"
+  [pending]="•|(.)"
+  [warning]="•|(!)"
+  [critical]="•|(!!)"
+  [busted]="⬤|(X)"
+  [unknown]="•|(?)"
+)
+
+declare -A TERM_DIAGRAM_ICON=(
+  [user]="👤|(U)"
+  [web]="🌐|(W)"
+  [mobile]="📱|(M)"
+  [auth]="🔐|(A)"
+  [database]="🗄|(D)"
+  [cache]="⚡|(C)"
+  [queue]="📨|(Q)"
+  [storage]="📦|(P)"
+  [service]="⚙|*"
+  [api]="🔌|(I)"
+  [search]="🔍|(S)"
+  [timer]="⏱|(T)"
+  [build]="🔨|(B)"
+  [hook]="🪝|(H)"
+  [log]="📄|(F)"
+)
+
+# Header indicator glyph (branch/⎇)
+TERM_GLYPH_BRANCH=""
+
+# Inline alert glyph (▲)
+TERM_GLYPH_ALERT=""
+
+# Empty-state tip glyph (💡)
+TERM_GLYPH_TIP=""
+
+# Spinner frame banks (set by term_init; arrays keep order).
+TERM_SPIN_WORKING=()
+TERM_SPIN_HEARTBEAT=()
 
+# ─── term_init ────────────────────────────────────────────────────────────
 term_init() {
   # TTY detection — stdout only.
   if [[ -t 1 ]]; then TERM_TTY=1; else TERM_TTY=0; fi
@@ -73,6 +137,15 @@ term_init() {
     TERM_TREE_BRANCH="+-"
     TERM_TREE_LAST="\`-"
     TERM_TREE_VERT="|"
+    TERM_PANEL_TL="+"
+    TERM_PANEL_BL="+"
+    TERM_PANEL_HRULE="-"
+    TERM_PANEL_TERM="*"
+    TERM_GLYPH_BRANCH="(b)"
+    TERM_GLYPH_ALERT="!"
+    TERM_GLYPH_TIP="(i)"
+    TERM_SPIN_WORKING=('|' '/' '-' '\')
+    TERM_SPIN_HEARTBEAT=('.' ':' '*' ':')
   else
     TERM_ICON_PENDING="⏳"
     TERM_ICON_READY="✅"
@@ -83,36 +156,73 @@ term_init() {
     TERM_TREE_BRANCH="├─"
     TERM_TREE_LAST="└─"
     TERM_TREE_VERT="│"
+    TERM_PANEL_TL="╭"
+    TERM_PANEL_BL="╰"
+    TERM_PANEL_HRULE="─"
+    TERM_PANEL_TERM="●"
+    TERM_GLYPH_BRANCH="⎇"
+    TERM_GLYPH_ALERT="▲"
+    TERM_GLYPH_TIP="💡"
+    TERM_SPIN_WORKING=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
+    TERM_SPIN_HEARTBEAT=('·' '∙' '•' '●' '•' '∙')
   fi
 
   if [[ "$TERM_COLOR" -eq 1 ]]; then
     TERM_C_GREEN=$'\033[32m'
     TERM_C_YELLOW=$'\033[33m'
+    TERM_C_ORANGE=$'\033[38;5;208m'
     TERM_C_RED=$'\033[31m'
     TERM_C_CYAN=$'\033[36m'
+    TERM_C_MAGENTA=$'\033[35m'
     TERM_C_DIM=$'\033[2m'
     TERM_C_OFF=$'\033[0m'
   else
-    TERM_C_GREEN=""; TERM_C_YELLOW=""; TERM_C_RED=""
-    TERM_C_CYAN=""; TERM_C_DIM=""; TERM_C_OFF=""
+    TERM_C_GREEN=""; TERM_C_YELLOW=""; TERM_C_ORANGE=""
+    TERM_C_RED=""; TERM_C_CYAN=""; TERM_C_MAGENTA=""
+    TERM_C_DIM=""; TERM_C_OFF=""
   fi
 }
 
-# term_color <name> <text...>  — wrap text in named color (green/yellow/red/cyan/dim).
+# ─── Color helper ─────────────────────────────────────────────────────────
+# term_color <name> <text...>
 term_color() {
   local name=$1; shift
   local code=""
   case "$name" in
-    green)  code="$TERM_C_GREEN" ;;
-    yellow) code="$TERM_C_YELLOW" ;;
-    red)    code="$TERM_C_RED" ;;
-    cyan)   code="$TERM_C_CYAN" ;;
-    dim)    code="$TERM_C_DIM" ;;
+    green)   code="$TERM_C_GREEN" ;;
+    yellow)  code="$TERM_C_YELLOW" ;;
+    orange)  code="$TERM_C_ORANGE" ;;
+    red)     code="$TERM_C_RED" ;;
+    cyan)    code="$TERM_C_CYAN" ;;
+    magenta) code="$TERM_C_MAGENTA" ;;
+    dim)     code="$TERM_C_DIM" ;;
   esac
   printf '%s%s%s' "$code" "$*" "$TERM_C_OFF"
 }
 
-# term_state_icon <STATE>  — echo glyph for a known state.
+# ─── Registry lookup ──────────────────────────────────────────────────────
+# term_emoji <registry_name> <key>  — returns Unicode glyph or ASCII fallback.
+# Internal helper; pass "BRAND", "HEALTH_GLYPH", "DIAGRAM_ICON".
+__term_lookup() {
+  local map=$1 key=$2 entry uni ascii
+  case "$map" in
+    BRAND)         entry="${TERM_BRAND[$key]:-}" ;;
+    HEALTH_GLYPH)  entry="${TERM_HEALTH_GLYPH[$key]:-}" ;;
+    DIAGRAM_ICON)  entry="${TERM_DIAGRAM_ICON[$key]:-}" ;;
+    *)             entry="" ;;
+  esac
+  [[ -z "$entry" ]] && { printf '%s' "?"; return; }
+  uni="${entry%|*}"
+  ascii="${entry#*|}"
+  if [[ "$TERM_ASCII_MODE" -eq 1 ]]; then printf '%s' "$ascii"
+  else printf '%s' "$uni"; fi
+}
+
+term_brand_glyph()    { __term_lookup BRAND        "$1"; }
+term_health_glyph()   { __term_lookup HEALTH_GLYPH "$1"; }
+term_diagram_icon()   { __term_lookup DIAGRAM_ICON "$1"; }
+
+# ─── Legacy state-icon helper (used by fleet.sh) ──────────────────────────
 term_state_icon() {
   case "$1" in
     RUNNING|PENDING)   printf '%s' "$TERM_ICON_PENDING" ;;
@@ -125,6 +235,8 @@ term_state_icon() {
   esac
 }
 
+# ─── Primitives ───────────────────────────────────────────────────────────
+
 # term_repeat <char> <n>
 term_repeat() {
   local ch=$1 n=$2 i out=""
@@ -132,7 +244,274 @@ term_repeat() {
   printf '%s' "$out"
 }
 
-# term_header <title> [meta]  — "── title ──────  meta"
+# term_truncate <text> <max_cols>  — ellipsis-truncate, append "…" or "..".
+term_truncate() {
+  local text=$1 max=$2
+  local len=${#text}
+  if [[ $len -le $max ]]; then printf '%s' "$text"; return; fi
+  local ell="…"
+  [[ "$TERM_ASCII_MODE" -eq 1 ]] && ell=".."
+  local elllen=${#ell}
+  printf '%s%s' "${text:0:$((max - elllen))}" "$ell"
+}
+
+# ─── Panel ────────────────────────────────────────────────────────────────
+
+# term_panel_open <emoji_key> <name> [right_indicator]
+#   ╭── ⚡ name ─────────  <indicator> ───●
+term_panel_open() {
+  local key=$1 name=$2 indicator=${3:-}
+  local emoji
+  emoji=$(term_brand_glyph "$key")
+  local left="${TERM_PANEL_TL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${emoji} $(term_color cyan "$name") "
+  local right=""
+  if [[ -n "$indicator" ]]; then
+    right=" $(term_color dim "$indicator") ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
+  else
+    right="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
+  fi
+
+  # Visible (color-stripped) widths to size the rule fill correctly.
+  local left_vis="${TERM_PANEL_TL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${emoji} ${name} "
+  local right_vis=""
+  [[ -n "$indicator" ]] && right_vis=" ${indicator} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" \
+                       || right_vis="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}"
+
+  local fill=$(( TERM_WIDTH - ${#left_vis} - ${#right_vis} ))
+  [[ $fill -lt 4 ]] && fill=4
+  local rule
+  rule=$(term_repeat "$TERM_PANEL_HRULE" "$fill")
+  printf '%s%s%s\n' "$left" "$(term_color cyan "$rule")" "$right"
+}
+
+# term_panel_close [hotkeys] [health_indicators]
+#   ╰── R refresh · L land · ? help ───── • daemon  • 17m ───●
+# `hotkeys`: pre-formatted "R refresh · L land · ? help" string.
+# `healths`: pre-formatted "• daemon  • 17m" string.
+term_panel_close() {
+  local hotkeys=${1:-} healths=${2:-}
+  local left="${TERM_PANEL_BL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${hotkeys} "
+  local right=""
+  if [[ -n "$healths" ]]; then
+    right=" ${healths} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
+  else
+    right="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}$(term_color cyan "$TERM_PANEL_TERM")"
+  fi
+
+  local left_vis="${TERM_PANEL_BL}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE} ${hotkeys} "
+  local right_vis=""
+  [[ -n "$healths" ]] && right_vis=" ${healths} ${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}" \
+                     || right_vis="${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_HRULE}${TERM_PANEL_TERM}"
+
+  local fill=$(( TERM_WIDTH - ${#left_vis} - ${#right_vis} ))
+  [[ $fill -lt 4 ]] && fill=4
+  local rule
+  rule=$(term_repeat "$TERM_PANEL_HRULE" "$fill")
+  printf '%s%s%s\n' "$left" "$(term_color cyan "$rule")" "$right"
+}
+
+# term_panel_vert  — emit a single body-line spacer "│"
+term_panel_vert() {
+  printf '%s\n' "$(term_color dim "$TERM_TREE_VERT")"
+}
+
+# ─── Body components ──────────────────────────────────────────────────────
+
+# term_section <state> <label> <count>
+#   ├── LABEL (n)   (label colored by state)
+term_section() {
+  local state=$1 label=$2 count=$3
+  local color=""
+  case "$state" in
+    RUNNING|PENDING|CONFLICT|WARN|warning) color="yellow" ;;
+    READY|LANDED|DONE|OK|healthy)          color="green" ;;
+    FAILED|ERROR|critical|alarm)           color="red" ;;
+    *)                                     color="" ;;
+  esac
+  local rendered_label="$label"
+  [[ -n "$color" ]] && rendered_label=$(term_color "$color" "$label")
+  printf '%s%s %s %s\n' \
+    "$(term_color dim "$TERM_TREE_VERT")" \
+    "$(term_color dim "$TERM_TREE_BRANCH$TERM_PANEL_HRULE")" \
+    "$rendered_label" \
+    "$(term_color dim "($count)")"
+}
+
+# term_summary_line <text>  — dim metadata branch
+#   ├── text
+term_summary_line() {
+  printf '%s%s %s\n' \
+    "$(term_color dim "$TERM_TREE_VERT")" \
+    "$(term_color dim "$TERM_TREE_BRANCH$TERM_PANEL_HRULE")" \
+    "$(term_color dim "$*")"
+}
+
+# term_leaf_line <connector> <name> <leaf_glyph> <meta> <age>
+#   │   ├── name              ●─●─●─◉    M4 ?1   12m
+# `connector` = ├── or └──
+term_leaf_line() {
+  local conn=$1 name=$2 leaf=$3 meta=${4:-} age=${5:-}
+  local trunc_name
+  trunc_name=$(term_truncate "$name" 28)
+  printf '%s   %s %-28s  %-14s %-10s %s\n' \
+    "$(term_color dim "$TERM_TREE_VERT")" \
+    "$(term_color dim "$conn$TERM_PANEL_HRULE")" \
+    "$trunc_name" \
+    "$leaf" \
+    "$(term_color dim "$meta")" \
+    "$(term_color dim "$age")"
+}
+
+# term_toast <emoji_key> <text>  — ├── ⚡ text   (dim cyan)
+term_toast() {
+  local key=$1; shift
+  local emoji
+  emoji=$(term_brand_glyph "$key")
+  printf '%s%s %s\n' \
+    "$(term_color dim "$TERM_TREE_VERT")" \
+    "$(term_color dim "$TERM_TREE_BRANCH$TERM_PANEL_HRULE")" \
+    "$(term_color cyan "$emoji $*")"
+}
+
+# term_alert <severity> <text>  — ▲ message (orange/red), as a sub-row
+# `severity` = warning | critical
+term_alert() {
+  local sev=$1; shift
+  local color="orange"
+  [[ "$sev" == "critical" ]] && color="red"
+  printf '%s   %s %s %s\n' \
+    "$(term_color dim "$TERM_TREE_VERT")" \
+    "$(term_color dim "$TERM_TREE_VERT")" \
+    "$(term_color "$color" "$TERM_GLYPH_ALERT")" \
+    "$*"
+}
+
+# ─── Leaf glyph builders ──────────────────────────────────────────────────
+
+# term_rail <commits_ahead> <head_state>
+#   head_state: HEAD | CONFLICT | EMPTY
+# Examples:
+#   term_rail 3 HEAD     → ●─●─●─◉
+#   term_rail 4 HEAD     → ●─●─●─●─◉
+#   term_rail 1 HEAD     → ●─◉
+#   term_rail 3 CONFLICT → ●─●─⊗
+#   term_rail 0 EMPTY    → ─
+term_rail() {
+  local n=$1 head=${2:-HEAD}
+  local commit="●"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && commit="*"
+  local link="─";   [[ "$TERM_ASCII_MODE" -eq 1 ]] && link="-"
+  local headg="◉";  [[ "$TERM_ASCII_MODE" -eq 1 ]] && headg="@"
+  local conflict="⊗"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && conflict="X"
+
+  if [[ $n -le 0 && "$head" == "EMPTY" ]]; then printf '%s' "$link"; return; fi
+
+  local out=""
+  local i
+  # n landed commits, joined by links
+  for (( i=0; i<n-1; i++ )); do
+    out="${out}$(term_color green "$commit")${link}"
+  done
+
+  # final glyph
+  case "$head" in
+    HEAD)
+      if [[ $n -ge 1 ]]; then out="${out}$(term_color green "$commit")${link}"; fi
+      out="${out}$(term_color yellow "$headg")"
+      ;;
+    CONFLICT)
+      if [[ $n -ge 1 ]]; then out="${out}$(term_color green "$commit")${link}"; fi
+      out="${out}$(term_color red "$conflict")"
+      ;;
+    *)
+      [[ $n -ge 1 ]] && out="${out}$(term_color green "$commit")"
+      ;;
+  esac
+  printf '%s' "$out"
+}
+
+# term_pip_bar <metric_type> <filled> <total>
+#   metric_type: progress | score | capacity
+#   filled / total are integers (e.g., 30, 100)
+term_pip_bar() {
+  local kind=$1 filled=$2 total=$3
+  local pip_full="▰"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && pip_full="#"
+  local pip_empty="▱"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && pip_empty="-"
+  local width=10
+  [[ "$total" -ne 100 && "$total" -gt 0 && "$total" -le 12 ]] && width=$total
+
+  # Pip count
+  local pips
+  if [[ "$total" -eq 100 ]]; then
+    pips=$(( filled / 10 ))
+  else
+    pips=$filled
+  fi
+  [[ $pips -lt 0 ]] && pips=0
+  [[ $pips -gt $width ]] && pips=$width
+
+  # Color selection
+  local color="green"
+  local pct=$(( total > 0 ? filled * 100 / total : 0 ))
+  case "$kind" in
+    progress) color="yellow"; [[ $pct -ge 100 ]] && color="green" ;;
+    score)    if   [[ $pct -lt 33 ]]; then color="red"
+              elif [[ $pct -lt 66 ]]; then color="yellow"
+              else color="green"; fi ;;
+    capacity) if   [[ $pct -ge 80 ]]; then color="red"
+              elif [[ $pct -ge 60 ]]; then color="yellow"
+              else color="green"; fi ;;
+  esac
+
+  local i out=""
+  for (( i=0; i<pips; i++ )); do out="${out}$(term_color "$color" "$pip_full")"; done
+  for (( i=pips; i<width; i++ )); do out="${out}$(term_color dim "$pip_empty")"; done
+  printf '%s' "$out"
+}
+
+# ─── Right-side furniture ─────────────────────────────────────────────────
+
+# term_health <state> <text>  — • text (colored bullet, with ⬤ for busted)
+# state: healthy|pending|warning|critical|busted|unknown
+term_health() {
+  local state=$1; shift
+  local glyph
+  glyph=$(term_health_glyph "$state")
+  local color=""
+  case "$state" in
+    healthy)  color="green" ;;
+    pending)  color="yellow" ;;
+    warning)  color="orange" ;;
+    critical) color="red" ;;
+    busted)   color="dim" ;;
+    *)        color="dim" ;;
+  esac
+  printf '%s %s' "$(term_color "$color" "$glyph")" "$*"
+}
+
+# term_hotkey <key> <verb>  — "R refresh"  (key in cyan)
+term_hotkey() {
+  printf '%s %s' "$(term_color cyan "$1")" "$2"
+}
+
+# ─── Spinners (live mode) ─────────────────────────────────────────────────
+
+# term_spinner_frame <family> <tick>  — return frame at `tick % frames`.
+# family: working | heartbeat
+term_spinner_frame() {
+  local fam=$1 tick=$2
+  local -a frames
+  case "$fam" in
+    working)   frames=("${TERM_SPIN_WORKING[@]}") ;;
+    heartbeat) frames=("${TERM_SPIN_HEARTBEAT[@]}") ;;
+    *)         printf '?'; return ;;
+  esac
+  local n=${#frames[@]}
+  printf '%s' "${frames[$(( tick % n ))]}"
+}
+
+# ─── Legacy / kept-for-compat helpers (used by older scripts) ─────────────
+
+# term_header <title> [meta]  — "── title ──────  meta" (legacy)
 term_header() {
   local title=$1 meta=${2:-}
   local glyph="─"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && glyph="-"
@@ -147,14 +526,12 @@ term_header() {
   fi
 }
 
-# term_divider [width]  — plain horizontal rule.
 term_divider() {
   local w=${1:-$TERM_WIDTH}
   local glyph="─"; [[ "$TERM_ASCII_MODE" -eq 1 ]] && glyph="-"
   printf '%s\n' "$(term_repeat "$glyph" "$w")"
 }
 
-# term_tree_item <icon> <label> [meta]  — "  <icon>  label                  meta"
 term_tree_item() {
   local icon=$1 label=$2 meta=${3:-}
   if [[ -n "$meta" ]]; then
@@ -164,40 +541,16 @@ term_tree_item() {
   fi
 }
 
-# Tree connectors — set by term_init via TERM_ASCII_MODE.
-TERM_TREE_BRANCH=""    # ├─  /  +-
-TERM_TREE_LAST=""      # └─  /  `-
-TERM_TREE_VERT=""      # │   /  |
-
-# Tree-control philosophy: the connectors (├─ │ └─) are the scaffold.
-# Icons and labels sit AFTER the connector, never between it and the
-# vertical line of its parent. To render a tree:
-#
-#   term_tree_node "" "$(term_tree_connector $i $last)" "⏳ RUNNING (3)"
-#   term_tree_node "│  " "$(term_tree_connector $j $last)" "feat/auth" "12m"
-#
-# `prefix` is what comes before this row's connector — built by walking
-# the ancestor chain and appending TERM_TREE_VERT+"  " for non-last
-# ancestors, or three spaces for last ancestors.
-
-# term_tree_connector <idx> <last_idx>  — echo branch or last glyph.
 term_tree_connector() {
   if [[ "$1" -eq "$2" ]]; then printf '%s' "$TERM_TREE_LAST"
   else printf '%s' "$TERM_TREE_BRANCH"; fi
 }
 
-# term_tree_indent <is_last>  — echo the 3-col continuation segment for
-# this ancestor: "│  " when more siblings follow, "   " when last.
 term_tree_indent() {
   if [[ "$1" -eq 1 ]]; then printf '   '
   else printf '%s  ' "$TERM_TREE_VERT"; fi
 }
 
-# term_tree_node <prefix> <connector> <label> [meta]
-#   prefix:    ancestor-chain string (built from term_tree_indent calls)
-#   connector: result of term_tree_connector for THIS row
-#   label:     visible text (may include leading icon — won't break the line)
-#   meta:      optional dim trailing text
 term_tree_node() {
   local prefix=$1 conn=$2 label=$3 meta=${4:-}
   if [[ -n "$meta" ]]; then
@@ -207,12 +560,10 @@ term_tree_node() {
   fi
 }
 
-# term_table_row <c1> <c2> <c3>  — fixed-width 3-col row.
 term_table_row() {
   printf '  %-2s  %-32s %-10s %s\n' "${1:-}" "${2:-}" "${3:-}" "${4:-}"
 }
 
-# term_empty <message>  — dim italic-ish empty state.
 term_empty() {
   printf '  %s\n' "$(term_color dim "($*)")"
 }

+ 46 - 39
skills/fleet-ops/scripts/fleet.sh

@@ -164,66 +164,73 @@ cmd_fleet() {
     state_buckets[$idx]="${state_buckets[$idx]}${branch}|${age}|${meta}"$'\n'
   done
 
+  # Daemon health for the footer
+  local daemon_state="busted"
+  if [[ -f "$PID_FILE" ]]; then
+    local pid
+    pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
+    if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
+      daemon_state="healthy"
+    fi
+  fi
+
+  # Footer composition (reused on every render path)
+  local hotkeys
+  hotkeys="$(term_hotkey R refresh) · $(term_hotkey L land) · $(term_hotkey '?' help)"
+  local healths
+  healths="$(term_health "$daemon_state" "daemon")"
+  [[ $total -gt 0 ]] && healths="$healths  $(term_health pending "$active active")"
+
   echo ""
-  term_header "fleet" "$total $([ "$total" -eq 1 ] && echo lane || echo lanes) · $active active"
+  term_panel_open fleet fleet "$TERM_GLYPH_BRANCH $BASE_BRANCH"
 
   if [[ $total -eq 0 ]]; then
-    echo ""
-    term_empty "no lanes — run: fleet init <name>..."
+    # Empty state: tip + suggested commands
+    term_panel_vert
+    term_panel_vert
+    printf '%s   %s\n' "$(term_color dim "$TERM_TREE_VERT")" "no lanes yet"
+    term_panel_vert
+    term_panel_vert
+    printf '%s   %s %s\n' "$(term_color dim "$TERM_TREE_VERT")" "$TERM_GLYPH_TIP" "to get started:"
+    term_panel_vert
+    printf '%s      1. fleet init <name>...\n' "$(term_color dim "$TERM_TREE_VERT")"
+    printf '%s      2. (work in each lane)\n'  "$(term_color dim "$TERM_TREE_VERT")"
+    printf '%s      3. fleet start\n'          "$(term_color dim "$TERM_TREE_VERT")"
+    term_panel_vert
+    term_panel_vert
+    term_panel_close "$(term_hotkey '?' help)" "$(term_health unknown "v2.4.9")"
     echo ""
     return
   fi
 
-  # Build list of non-empty group indices so we know which is "last" at
-  # the top level — the tree's vertical needs to terminate cleanly.
-  local active_groups=()
+  # Summary branch + breath
+  term_panel_vert
+  term_summary_line "$total $([ "$total" -eq 1 ] && echo lane || echo lanes) · $active active"
+  term_panel_vert
+
+  # State sections with leaves underneath
   local i
   for i in 0 1 2 3 4; do
-    [[ ${state_counts[$i]} -gt 0 ]] && active_groups+=("$i")
-  done
-
-  local g_idx=0
-  local g_last=$(( ${#active_groups[@]} - 1 ))
-  for i in "${active_groups[@]}"; do
     local n=${state_counts[$i]}
+    [[ $n -eq 0 ]] && continue
     local state=${order[$i]}
 
-    # Group line — connector + plain label. NO icon at the junction:
-    # a glyph here breaks the eye-line of the tree's vertical. State is
-    # carried by label + color (and the per-leaf glyph if needed).
-    local g_conn group_label
-    g_conn=$(term_tree_connector "$g_idx" "$g_last")
-    case "$state" in
-      RUNNING|PENDING)  group_label=$(term_color yellow "$state") ;;
-      READY)            group_label=$(term_color green  "$state") ;;
-      LANDED|DONE|OK)   group_label=$(term_color green  "$state") ;;
-      FAILED|ERROR)     group_label=$(term_color red    "$state") ;;
-      CONFLICT|WARN)    group_label=$(term_color yellow "$state") ;;
-      *)                group_label="$state" ;;
-    esac
-    term_tree_node "" "$g_conn " "$group_label" "($n)"
-
-    # Children indent = continuation of this group's connector.
-    local child_prefix
-    if [[ $g_idx -eq $g_last ]]; then
-      child_prefix=$(term_tree_indent 1)
-    else
-      child_prefix=$(term_tree_indent 0)
-    fi
+    term_section "$state" "$state" "$n"
 
     local lines="${state_buckets[$i]}"
     local c_idx=0 c_last=$((n - 1))
     local branch age meta
     while IFS='|' read -r branch age meta; do
       [[ -z "$branch" ]] && continue
-      local c_conn meta_str="$age"
-      c_conn=$(term_tree_connector "$c_idx" "$c_last")
-      [[ -n "$meta" ]] && meta_str="$age  $meta"
-      term_tree_node "$child_prefix" "$c_conn" "$branch" "$meta_str"
+      local c_conn
+      if [[ $c_idx -eq $c_last ]]; then c_conn="$TERM_TREE_LAST"; else c_conn="$TERM_TREE_BRANCH"; fi
+      term_leaf_line "$c_conn" "$branch" "─" "${meta:-}" "$age"
       c_idx=$((c_idx+1))
     done <<< "$lines"
-    g_idx=$((g_idx+1))
+    term_panel_vert
   done
+
+  term_panel_close "$hotkeys" "$healths"
   echo ""
 }