git:20260930.40f4441 to git:20261002.36d4fb0

2 added, 127 removed. Audit A to B.

- # tui/: the stakeholder terminal surface
-
- An Ink 7 + React 19 app, bundled by esbuild into one file (`dist/tui-app.js`)
- that ships inside the humanish package and is loaded on demand by
- `humanish tui`. Every other humanish command is built so an agent can drive it;
- this is the one that takes the screen and waits for a person.
-
- The bundle is a view layer. Everything humanish knows how to do is injected
- across `src/tui/contract.ts` (repo root `src/`, not this workspace), so this
- app never becomes a second implementation of "what is a run, which lab does it
- belong to, is it alive" that can drift from `humanish runs`.
-
- ## Commands
-
- From the repo root (pnpm workspace):
-
- - `pnpm --filter humanish-tui typecheck`
- - `pnpm --filter humanish-tui test` runs the navigation and text-golden tests.
- - `pnpm --filter humanish-tui build` writes the bundle to `tui/dist/tui-app.js`.
- - `pnpm tui:smoke` loads the built bundle and renders a real frame. It reads the repo root's
- `dist/tui-app.js` and `dist/tui/contract.js`, which only the root `pnpm build` writes, so run
- that first; the workspace build alone is not enough.
- - `UPDATE_TUI_GOLDENS=1 pnpm --filter humanish-tui test` regenerates the goldens. Do it
- deliberately, and read the diff before committing.
-
- ## Layout
-
- - `src/entry.tsx`: the bundle's only export (`startTui`); everything else is reached through
- it.
- - `src/app.tsx`: data load, keyboard handling, and the frame chrome.
- - `src/navigation.ts`: the screen stack as a pure reducer, so the whole navigation model is
- testable without a terminal.
- - `src/screens/`: six screens. `labs`, `lab` and `run` are the main path; `all-runs` opens from
- the row below the last lab; `help` opens on `?`; `connections` opens on `c`.
- - `src/testing/render-to-text.tsx`: the in-repo render harness.
- - `tests/golden/*.txt`: committed frames at 80 and 45 columns.
- - `build.mjs`: esbuild config, including the two bundling workarounds below.
-
- ## How to verify a change
-
- A ladder. Start at the top; only descend when the rung above genuinely cannot
- answer the question.
-
- 1. **Text goldens** (`tests/`) are the default, and where narrow-width work
- belongs. These render through real yoga layout at exact widths against a
- fake stdout, so they catch the entire class of layout bug (a status column
- colliding with a name, a path wrapping to two lines, a row overflowing a
- phone-width screen) with no terminal, no subprocess, and no sleeps. They
- wait on frame predicates, never timers. A fixed sleep captures whichever
- frame the scheduler happened to reach, which is how TUI suites become flaky.
- Run them at least once as `CI=true pnpm --filter humanish-tui test` before
- pushing. Ink asks `is-in-ci` whether the environment is interactive and, when
- it decides not, writes only the final frame at unmount, with no erase
- sequences and no intermediate renders. A suite that waits for frames then passes locally
- and fails every render test in CI. Both the harness and `entry.tsx` pass
- `interactive: true` to stop an environment variable deciding this, and a test
- pins it, but the habit is cheap and catches the next divergence of this kind.
-
- 2. **Bundle smoke** (`pnpm tui:smoke`, after the root `pnpm build`) loads the built
- artifact with fake TTY streams. This is the only thing that catches load-time failures, and
- load-time failures are the characteristic way a bundled Ink app breaks: it
- builds clean, passes every unit test, and dies the first time a user runs it.
- 3. **A real terminal** is only for "does this work for a human", never for
- layout. See the hazard below before reaching for tmux.
-
- ## The tmux hazard (read this before automating a real terminal)
-
- On 2026-08-19 this workspace's own testing crashed the machine's tmux server
- twice, taking every session on it down with it. The cause is understood and
- worth not rediscovering:
-
- - `window-size manual` makes the size of a new window come from `default-size`
- (see `man tmux`). tmux 3.4 walks that path in `clients_calculate_size` while
- the window does not exist yet and dereferences a NULL `struct window *`.
- Both crashes faulted at a byte-identical address, and it reproduces on an
- isolated socket in three commands, every time.
- - `-g` is what made it catastrophic rather than local. `tmux set-option -t SESS
- -g window-size manual` sets the option server-wide; the `-t` does not scope
- it. One server means every session on the machine dies together.
- - The obvious replacement fails on a shared server. `new-session -x 36
- -y 14` alone yielded a 59-column pane here, because the default `window-size
- latest` sizes the window to the most recently active client and overrides
- `-x`/`-y`. Measure `#{pane_width}` before trusting a width.
-
- So, if a real terminal is genuinely needed:
-
- - Prefer a plain PTY over a multiplexer. A PTY has no server, no global option
- surface, and no blast radius beyond itself. This works today and needs
- nothing installed:
-
- ```bash
- timeout 25 script -qec 'stty cols 92 rows 30; node dist/cli.js tui' /dev/null
- ```
-
- The `stty` is not optional: `script` with no controlling terminal reports
- `isTTY=true` but `columns=0`, so a size set any other way is silently ignored.
- Use `node-pty` instead if a check needs to send keys and read frames back, and
- `@xterm/headless` if output ever needs parsing for colour, cursor or
- alt-screen assertions. The terminal-testing tools (xterm.js/VS Code, termless)
- converge on that pairing.
-
- - If tmux is still the right tool, put it on its own socket
- (`tmux -L humanish-tui …`) so a tmux bug can only kill that server, and size
- windows with `resize-window -t <session> -x W -y H`, which is scoped to the
- window. Never `-g`.
-
- ## Rules
+ @README.md
- - Ink's `<Text>` props are not optional-with-undefined and this repo compiles
- with `exactOptionalPropertyTypes`, so pass colour through `src/text-props.ts`
- (`{...color(maybe)}`) rather than `color={maybe}`.
- - Put spaces that matter in terminal output inside a string literal:
- `<Text>{" none yet"}</Text>`. oxfmt, like Prettier, collapses runs of spaces in
- JSX text, which is harmless in a browser and changes an Ink layout.
- - Measure terminal size from Ink's own `useStdout`, never from a prop. Two
- different stdout objects means laying out to one width and drawing into
- another, silently, and only visibly at narrow widths.
- - Two bundling workarounds in `build.mjs` are load-bearing; removing either
- produces a bundle that builds and then crashes on first run:
- `react-devtools-core` is an optional peer Ink imports at module scope and
- must be stubbed (marking it `external` leaves a static import Node resolves
- eagerly), and several Ink dependencies are still CommonJS, so the ESM output
- needs a `createRequire` banner or esbuild's `__require` shim throws on
- `require("node:*")`.
- - Keep the surface honest in the same way the evidence surfaces are: an unknown
- cost is "not recorded" and never `$0.00`, a statistic carries its denominator,
- truncation is visible, and a project that cannot be read says so instead of
- rendering an empty one.
+ The rules for working in `tui/` are in [README.md](README.md).