hosystem Engagements

ho-04.4 — The ingest dashboard

created 2026-07-02
status superseded
superseded-by ho-04.6 (plain-prompt init)
type ho-document
project sharibako
ho 04.4
kamae 5
shape ha
agent-tasks
  • Ho-04.4-AT-01.md
  • Ho-04.4-AT-02.md
  • Ho-04.4-AT-03.md
supersedes
  • ho-04.2 Decision 1 (per-key arrow walk)

sharibako init shows every secret it finds on one screen and lets the practitioner route all of them from that single view — set each key's fate, bulk-apply to a marked set or the whole list, then commit once. This replaces the per-key arrow walk from ho-04.2, which read fine at three keys and fell apart at twelve.

This is motion off ho-04.2's . That dogfooded the per-key walk against a real twelve-key .env and found the model structurally wrong at scale: each answered key's menu stayed stacked on screen, move-to-shared recorded a corrupted slug, and there was no bulk action for a .env that size. This ho ho-04.2 Decision 1 — the per-key sequential walk — and replaces it with the one-page dashboard. ho-04.2 is not edited; it stands as the record of what was known then. This ho is the response to what the surfaced.

Replacing the prompt wholesale also retires two bugs ho-04.2's Reflect documented as known-in-main, rather than patching a surface that's being thrown out:

Both die as a byproduct of the dashboard owning its screen and its input.

Out of scope:

What carries forward unchanged from ho-04.2: first-run key generation (Decision 5), Touch-ID-free ingest (Decision 2), scope-ID collision reuse (Decision 3), re-init reconcile (Decision 7), source-.env untouched (Decision 4). Those decisions held under the dogfood. InitCommand keeps that scaffolding; only the decision-collection call changes from the per-key prompt to the dashboard.


Phase 1 — Think

The interaction model was resolved in conversation with the practitioner. What follows records the decisions.

Decision 1 — One page: every key a row, navigate and set in place

init renders all detected keys as a vertical list, one row per key, and the practitioner routes each from that single view. This supersedes ho-04.2 Decision 1's per-key sequential prompt.

Each row shows the key name and its current choice — one of import / link / move / leave / skip — rendered as a colour change plus a , not a reverse-video block (the reverse-video block read as heavy at twelve rows). The active row is marked distinctly (a cursor glyph and/or highlight). Rows are spaced for legibility rather than packed.

Navigation:

Initial state honours the Materializer's suggestions: a key with nameMatchedSharedID set starts on link with its matched entry pre-resolved and named in the row; every other key starts on import (the dominant real choice). So the first frame already reflects the sensible default and the common case is often "glance, commit."

suggestedKeysNeedingValues and parseWarnings are context, not decidable rows — they render as a read-only banner (keys that appeared only in .env.example with no value; any parse warnings the ingest collected), placed above or below the table. They inform; they are never navigated.

Decision 2 — Bulk: space marks, a applies to the marked set or the whole list

A large .env wants one motion that sets many keys at once. The model layers a lightweight selection onto Decision 1's per-row control:

ALL offers the target-free actions only — import / leave / skip. link and move are excluded from bulk because each needs its own target: link needs a specific shared entry, move needs a distinct new slug per key. Bulking many keys onto one shared target is incoherent, so those two stay per-row (←/→ plus their secondary input, Decision 4). This keeps ALL fast and unambiguous — the overwhelmingly common bulk is "import everything," which ALL does in two keystrokes (a, pick import).

ALL overwrites: applying it re-sets the affected rows regardless of their current choice, including name-matched rows. That is deliberate — ALL is invoked explicitly, so overwriting a default is a chosen act, not a stumble.

Decision 3 — Alternate screen buffer, cursor-addressed redraw

The dashboard runs on the terminal's alternate screen (smcup/rmcup — the mode vim, less, and htop use): on entry it switches to a throwaway full screen, on exit it restores the prior screen and the practitioner's scrollback is untouched. This makes the frame-stacking failure structurally impossible — the dashboard cannot pollute scrollback because it never draws into it.

Within the alternate screen the table is redrawn cursor-addressed (position, clear, paint) on each keystroke. Restore is SIGINT-safe: the module-level saved-termios + signal-handler pattern already in InteractiveSelect extends to also leave the alternate screen on Ctrl-C, so an interrupt returns a cooked terminal on the original screen, never a mangled one.

The renderer honours the project's colour discipline — TerminalDetector/OutputRenderer conventions, degrading to plain markers when colour is off.

Decision 4 — Secondary inputs collected inside the dashboard

link and move each need a second input, and the old code collected them wrong — it dropped out of raw mode to a bare readLine() with stray arrow bytes still buffered, which is how move-to-shared recorded a corrupted slug. The dashboard collects both inside its own render loop, owning the input the whole time:

Owning input end-to-end is what retires the corruption bug — it is a construction property of the dashboard, not a patch.

Decision 5 — A new widget; InteractiveSelect kept, InteractiveIngestPrompt retired

The dashboard is a new full-screen widget, IngestDashboard, not an extension of InteractiveSelect. InteractiveSelect answers one question with a single-select list; the dashboard holds persistent multi-row state on an alternate screen and collects secondary inputs. Growing one into the other would overload a small, working widget with a mode flag and two behaviours. InteractiveSelect stays exactly as it is, still used for the first-run "Generate a key? [Y/n]" and scope-type prompts that bracket ingest.

The per-key TTY source, InteractiveIngestPrompt, is superseded — its whole job was the per-key walk. init routes through a new IngestDecisionSource implementation driven by the dashboard, and InteractiveIngestPrompt (with its buggy resolveMove/resolveLink) is removed. The IngestDecisionSource protocol and ScriptedIngestDecisionSource are untouched.

This decision draws the decomposition seam (Phase 2): the reusable dashboard widget and its decision source are one task; wiring init to it and retiring the old prompt is the next.

Decision 6 — Navigate and set freely, commit once, cancel cleanly

Nothing is written until the practitioner commits. Enter commits the whole screen: it collapses the dashboard back to the normal terminal, prints a one-line summary of what's about to happen (counts per action), asks a single confirmation, and on yes calls acceptIngest exactly once with the full [KeyDecision] set. q/Esc cancels — restores the terminal, writes nothing, and init exits as a clean cancel (no scope, no marker). There are no partial writes and no mid-walk commits; the whole routing is one atomic decision handed to acceptIngest, the same call ho-04.2 already proved out.

Decision 7 — The IngestDecisionSource seam holds

The dashboard is a third implementation of IngestDecisionSource.decisions(for:sharedIDs:), alongside the scripted double and the (now-retired) per-key prompt. acceptIngest, KeyDecision, and ProposedScope are unchanged; the protocol signature is unchanged; ScriptedIngestDecisionSource still drives InitCommand's end-to-end tests through every decision branch without a terminal. Non-TTY input still refuses via CLIError.notInteractiveTerminal before any raw read — the dashboard checks isInteractive() on entry exactly as InteractiveSelect does. The raw-mode renderer (byte reads, alternate-screen escapes, termios) is coverage-excluded with a comment; the decision state machine — navigation, cycle, mark, ALL application, secondary-input resolution, and the final map to [KeyDecision] — is driven through injected readByte/write and fully covered.

Deferred to execution


Phase 2 — Execute

Two agent tasks, the same two- rhythm as ho-04.2: the reusable full-screen machinery first, verifiable in isolation, then the command that drives it and the retirement of the old prompt.

Ho-04.4-AT-01 — IngestDashboard widget + dashboard decision source

The IngestDashboard full-screen widget — alternate-screen raw-mode table, ↑/↓ navigation, ←/→ per-row cycle, space-mark, a ALL over the marked-or-all set, in-dashboard link pick and move slug field, SIGINT-safe restore, non-TTY refusal — and the IngestDecisionSource implementation that builds it from a ProposedScope and maps the result to [KeyDecision]. Pure terminal-UX infrastructure with its own acceptance criteria, testable through injected readByte/write before InitCommand is rewired. The raw-mode renderer is coverage-excluded; the state machine carries the coverage.

/agent-tasks/Ho-04.4-AT-01.md

Ho-04.4-AT-02 — Rewire InitCommand, retire InteractiveIngestPrompt

Point InitCommand at the dashboard decision source in place of InteractiveIngestPrompt, remove InteractiveIngestPrompt and its tests, and keep every other part of the init flow (first-run key generation, scope-ID/type prompts, reconcile, reporting) intact. End-to-end tests keep running through ScriptedIngestDecisionSource. Depends on AT-01.

ho-process/agent-tasks/Ho-04.4-AT-02.md

Ho-04.4-AT-03 — Dogfood corrections: viewport, cycle-without-trap, legibility

The first manual dogfood (twelve-key scratch .env, real terminal) landed the dashboard but found it unusable as built: content overflowed a short terminal with no scroll, cycling ←/→ onto move or link dropped the user into a modal input and trapped them there, the key column read as a monotone wall, and the footer wording ("commit", "cancel") was unclear. A corrective task inside this still-open ho: a scrolling viewport that keeps the active row visible at any height; cycling that only changes the label with move slug and link target collected at commit in cancelable prompts (and a lone-Esc read timeout so Esc no longer hangs); coloured key names; reworded footer (move / change / select / bulk / save / quit). All inside IngestDashboard; run() still returns [ResolvedChoice], so DashboardIngestPrompt and InitCommand are untouched.

ho-process/agent-tasks/Ho-04.4-AT-03.md

Testing and iteration approach

AT-01 lands and tests green before AT-02 opens — the dashboard's state machine and secondary-input collection are verifiable through a scripted readByte sequence and captured write frames, no init command required. AT-02 rewires init and is tested end-to-end through ScriptedIngestDecisionSource returning a mix of all five decisions, asserting the vault and marker states acceptIngest produces — the same assertions ho-04.2's InitCommand tests already make, since only the decision source changed. The alternate-screen renderer is coverage-excluded with a comment; every decision branch is covered through the interface. Coverage floor stays ≥ 85% on the new CLI code with the raw-mode renderer honestly excluded, consistent with ho-04 and ho-04.2.

One manual dogfood pass after AT-02: init against the same real multi-key .env that broke the per-key walk, driving the dashboard on a real terminal — set several keys, mark a subset and bulk-import, route one key to move (confirming the slug is now clean), commit once, and confirm the vault, the marker, and an untouched source .env. This is where "one page, not a slog" gets its only real test; the automated suite verifies logic, not feel.

Done means


Phase 3 — Reflect

The dashboard was built, dogfooded, found unusable, and superseded before it became init's real surface. The and the lesson:

What was built

AT-01 landed the IngestDashboard widget and DashboardIngestPrompt source — 405 tests, both linters clean (19887bf). AT-02 rewired InitCommand to it and retired InteractiveIngestPrompt (0d6c536). The IngestDecisionSource seam held exactly as designed: InitCommand's end-to-end tests never changed when the live source swapped.

What the dogfood found

Driven on a real terminal against a twelve-key .env, the dashboard did not work:

405 passing tests and it trapped the practitioner on the first keystroke. The tests verified the state-machine logic, which was correct. They could not verify feel, which was wrong.

Where the correction went

AT-03 was authored to fix all four (viewport, cycle-without-trap with at-commit collection, an Esc read-timeout, legibility). Its agent could not complete — the response exceeded the 32k output-token cap. The task was too large to produce in one pass, which is itself the evidence.

The lesson — the machine was wrong, not the details

Three signals converged: the third build of the same surface, a fully-tested implementation that was unusable on contact, and a correction too large to complete. The honest read: a -rolled, full-screen, scrolling, raw-mode TUI is the wrong amount of machine for a command run once per project. It is ~600 lines of termios to make a terminal imitate a spatial one-page table — something the GUI (ho-06) gets for free from SwiftUI (a List of Pickers). The framing that the dashboard was "the closer analog" to the GUI (carried from ho-04.2's Reflect) was backwards: it pushed the CLI to mirror the GUI's , which is exactly what over-built it. Each surface should be idiomatic to its medium and meet at the library seam — Materializer.ingest / acceptIngest / KeyDecision — not at the UX metaphor.

The process miss underneath: we polished before validating. A dogfood should hit a rough version, not a finished 600-line one. The loop was inverted — build fully, discover it's wrong, build again.

What this hands forward


Authored: 2026-07-02 (Think ). Execution: AT-01 (19887bf), AT-02 (0d6c536); AT-03 authored, not completed — superseded. Reflect: complete 2026-07-02 — dashboard superseded by ho-04.6 (plain prompt).

Rendered from the corpus, verbatim · source on GitHub →

ingested: sharibako @ a97b22af9b61 · ho-system @ 79e96b801a13 · the glossary · the colophon