diff --git a/docs/wiki/glasspane.md b/docs/wiki/glasspane.md index 420130c..093879a 100644 --- a/docs/wiki/glasspane.md +++ b/docs/wiki/glasspane.md @@ -18,17 +18,21 @@ Glasspane doesn't just relay raw agent events. It ingests JSONL lines and transitions a **named pane** through a finite set of states: ``` -Idle → Working → Done +Idle → Working → Blocked → Done ↳ Error - ↳ Stalled (no events within a timeout window) ``` -The `AgentState` enum (`Idle, Working, Done, Error, Stalled`) is deliberately +The `AgentState` enum (`Idle, Working, Blocked, Done, Error`) is deliberately small. It captures what a supervisor needs to know — "is the agent working? -stuck? finished?" — without encoding agent-specific semantics. Events that don't -change the state (e.g. a usage report from zot) are recorded in the pane's +blocked? finished?" — without encoding agent-specific semantics. Events that +don't change the state (e.g. a usage report from zot) are recorded in the pane's metadata but don't affect the state machine. +`Stalled` is **not** a sixth variant — it is a derived flag: a pane is stalled +when no event has arrived within `DEFAULT_STALL_AFTER` (4 hours). Derived +attention (Error / Blocked / Stalled) is covered by +[operator-attention](./operator-attention.md). + **Why not just tail the log**: raw event logs are agent-specific and change over time (zot adds new event types). The state machine is a stable contract that the daemon, TUI, and client CLI can all rely on. @@ -93,21 +97,11 @@ that's fundamentally about current state, not event delivery. ## Usability roadmap (TODO) -Glasspane has the supervision spine — a stable state machine and a snapshot API. -The open work is operator-facing: making *"does this agent need me right now?"* -the primary, impossible-to-miss object. These are captured as direction, not yet -built. The data mostly already exists; the work is surfacing and pushing it. -Ideas drawn from agent-cockpit terminal UIs; wiring is our own. Working notes, -reference shortcut vocabulary, and a draft keymap live in -[`../GLASSPANE-TUI-ENHANCEMENTS.md`](../GLASSPANE-TUI-ENHANCEMENTS.md). - -### Attention as a first-class derived signal - -Today a pane is `Idle/Working/Done/Error/Stalled`. Promote "needs the operator" -into an explicit **attention flag** derived from the existing states (`Error`, -`Stalled`, and a future "waiting for input") rather than a sixth state. The state -machine stays small; attention is a view over it. The TUI highlights attention -rows; the daemon and notifier read the same flag. +The **attention** half of this roadmap shipped: the derived attention +predicate, the TUI attention bar / jump keys / filter / row highlight, and +edge-triggered terminal-capture alerts. See +[operator-attention](./operator-attention.md) for the shipped system. What +remains here is the genuinely-unbuilt direction. ### Push notifications outbound, not just on-screen @@ -118,12 +112,6 @@ already provisioned). An explicit `colibri notify`-style path — or a glasspane event type that a zot/Pi hook fires — lets an agent say "I'm blocked" rather than relying only on inferred state. Highest real-world impact item. -### Jump-to-next-attention navigation in the TUI - -A keybinding in `colibri-tui` that cycles to the next attention pane. Trivial -given the attention flag; large ergonomic win when supervising many agents. -→ `crates/colibri-glasspane-tui/src/main.rs` - ### Richer pane rows (context at a glance) Glasspane already stashes non-state events in pane metadata. Surface that in the @@ -149,4 +137,5 @@ control and needs its own design pass. ## See also - [agent-harness](./agent-harness.md) — the zot/Colibri split that Glasspane observes +- [operator-attention](./operator-attention.md) — the shipped attention/alert layer over this state machine - [naming-decisions](./naming-decisions.md) — `pi_session_id → session_id`, `pi_type → event_type` diff --git a/docs/wiki/index.md b/docs/wiki/index.md index 4358e83..73a3183 100644 --- a/docs/wiki/index.md +++ b/docs/wiki/index.md @@ -43,25 +43,27 @@ warning. ## Pages -| Page | What it covers | -| ----------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| [agent-harness](./agent-harness.md) | The zot (agent) + Colibri (control plane) split; autospawn + RPC driver | -| [agent-events-reference](./agent-events-reference.md) | Per-harness zot event reference, Glasspane mappings, and verified transcript fields | -| [cost-model](./cost-model.md) | Byte-stable prefixes, cache-hit metering, auto-escalation, T14 compaction | -| [glasspane](./glasspane.md) | Agent state machine, JSONL streaming, AgentRuntime taxonomy, snapshot API | -| [headroom-sidecar](./headroom-sidecar.md) | Optional tool-result compression sidecar and its Unix-socket protocol | -| [jail-confinement](./jail-confinement.md) | Persistent vs ephemeral jails, priv-mode policy, reuse of spawner confinement for MCP servers | -| [mother-hive](./mother-hive.md) | Mother MCP architecture — forced-command SSH, single-home-in-colibri, peer auth, key-on-seed | -| [naming-decisions](./naming-decisions.md) | Ledger of harness-neutral / architecture renames — shipped and in-flight | -| [layered-soul](./layered-soul.md) | How Colibri consumes the layered-soul reviewed-context repo today vs planned | -| [task-board](./task-board.md) | Capability match scoring, cron scheduling, intake drain, SQLite backing | -| [quality-gates](./quality-gates.md) | `ci-checks.sh` as the pre-merge gate; why drift reached `main` before | -| [contracts](./contracts.md) | Stable JSON schemas (run-manifest, runtime-inventory, provider-smoke), golden tests | -| [store-schema](./store-schema.md) | SQLite coordination schema and migration discipline | -| [external-mcp](./external-mcp.md) | MCP bridge for editors + external stdio MCP host; read/write/external-call gates | -| [operator-cli](./operator-cli.md) | The `colibri` CLI as a thin typed Unix-socket client over the daemon API | -| [tui](./tui.md) | Terminal dashboard client (colibri-tui) vs the colibri-glasspane state machine | -| [runtime-inventory](./runtime-inventory.md) | Host runtime inventory + watchdog status reader; additive, read-only integrations | -| [skills-catalog](./skills-catalog.md) | Read-only runtime consumer for reviewed Clawdie-AI skill artifacts | -| [vault-provision](./vault-provision.md) | Vaultwarden-driven env-file provisioning into jails after agent spawn | -| [deployment](./deployment.md) | Host installer (clawdie): ZFS layout, rc.d/systemd service, dry-run safety | +| Page | What it covers | +| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| [agent-harness](./agent-harness.md) | The zot (agent) + Colibri (control plane) split; autospawn + RPC driver | +| [agent-events-reference](./agent-events-reference.md) | Per-harness zot event reference, Glasspane mappings, and verified transcript fields | +| [cost-model](./cost-model.md) | Byte-stable prefixes, cache-hit metering, auto-escalation, T14 compaction | +| [glasspane](./glasspane.md) | Agent state machine, JSONL streaming, AgentRuntime taxonomy, snapshot API | +| [operator-attention](./operator-attention.md) | The derived "needs the operator" view: attention predicate, TUI bar/jump/filter, edge-triggered terminal alerts | +| [headroom-sidecar](./headroom-sidecar.md) | Optional tool-result compression sidecar and its Unix-socket protocol | +| [jail-confinement](./jail-confinement.md) | Persistent vs ephemeral jails, priv-mode policy, reuse of spawner confinement for MCP servers | +| [mother-hive](./mother-hive.md) | Mother MCP architecture — forced-command SSH, single-home-in-colibri, peer auth, key-on-seed | +| [naming-decisions](./naming-decisions.md) | Ledger of harness-neutral / architecture renames — shipped and in-flight | +| [layered-soul](./layered-soul.md) | How Colibri consumes the layered-soul reviewed-context repo today vs planned | +| [task-board](./task-board.md) | Capability match scoring, cron scheduling, intake drain, SQLite backing | +| [quality-gates](./quality-gates.md) | `ci-checks.sh` as the pre-merge gate; why drift reached `main` before | +| [contracts](./contracts.md) | Stable JSON schemas (run-manifest, runtime-inventory, provider-smoke), golden tests | +| [store-schema](./store-schema.md) | SQLite coordination schema and migration discipline | +| [external-mcp](./external-mcp.md) | MCP bridge for editors + external stdio MCP host; read/write/external-call gates | +| [operator-cli](./operator-cli.md) | The `colibri` CLI as a thin typed Unix-socket client over the daemon API | +| [tui](./tui.md) | Terminal dashboard client (colibri-tui) vs the colibri-glasspane state machine | +| [terminal](./terminal.md) | Terminal capability decision (Kitty, extended-key reporting, tmux passthrough, SSH terminfo) | +| [runtime-inventory](./runtime-inventory.md) | Host runtime inventory + watchdog status reader; additive, read-only integrations | +| [skills-catalog](./skills-catalog.md) | Read-only runtime consumer for reviewed Clawdie-AI skill artifacts | +| [vault-provision](./vault-provision.md) | Vaultwarden-driven env-file provisioning into jails after agent spawn | +| [deployment](./deployment.md) | Host installer (clawdie): ZFS layout, rc.d/systemd service, dry-run safety | diff --git a/docs/wiki/operator-attention.md b/docs/wiki/operator-attention.md new file mode 100644 index 0000000..8db0410 --- /dev/null +++ b/docs/wiki/operator-attention.md @@ -0,0 +1,126 @@ +# Operator attention — "does this agent need me right now?" + +← [index](./index.md) + +## What this is + +Glasspane's supervision spine answers _"what state is this agent in?"_. Attention +is the layer on top that answers the question an operator actually asks first: +_"does any agent need me **right now**?"_ It is a **derived view** over the state +machine and the terminal, surfaced in the TUI and backed by edge-triggered +alerts — not a sixth state, not a new subsystem. + +## Decisions + +### Attention is a view, not a state + +`AgentState` stays small (`Idle`, `Working`, `Blocked`, `Done`, `Error`). +"Needs the operator" is a free predicate over it, not another variant: + +```rust +fn needs_attention(pane: &Pane) -> bool { + pane.state == AgentState::Error + || pane.state == AgentState::Blocked + || pane.stalled +} +``` + +`Blocked` is included because the state-machine doc comment says it means +"waiting on steering / approval / input" — i.e. the agent is parked on the +operator. One predicate, consumed by the attention bar, the filter, and the +jump keys, so the definition changes in one place. + +`stalled` is itself derived — a pane is stalled when no event has arrived +within `DEFAULT_STALL_AFTER` (4 hours). It is rare on purpose: attention is +mostly Errors and Blocked panes; Stalled is the "something is deeply wrong" +escalation, not a frequent one. + +→ [`crates/colibri-glasspane/src/lib.rs`](../../crates/colibri-glasspane/src/lib.rs) +(`AgentState`, `SupervisedPane::is_stalled_at`, `DEFAULT_STALL_AFTER`) + +### The TUI makes attention impossible to miss + +When any pane in the **current view** needs attention, the normal header is +replaced by a red-bordered attention bar listing the offending panes. Rows that +need attention get an inverted background; the cursor inverts again so the +operator can still see which one is selected. Two jump keys (`n` / `N`) cycle +forward/backward through attention panes with wrapping, and `a` toggles an +attention-only filter. All three operate over the already-filtered pane set. + +**Filter composition is AND.** Attention filter composes with the session +filter, so the bar reflects only what the operator is looking at. A 2026-06 +bug shipped where `has_attention` was computed from the _unfiltered_ snapshot: +an error in session `s2` lit the bar while viewing session `s1`, and the bar's +own `filtered_panes()` early-return then drew nothing — so the operator lost +their header to a blank red box. Fixed by computing attention from +`filtered_panes()`; covered by a cross-session render test. + +→ [`crates/colibri-glasspane-tui/src/main.rs`](../../crates/colibri-glasspane-tui/src/main.rs) +(`needs_attention`, `render_attention_bar`, `attention_indices`) + +### Terminal capture is the complementary signal + +The state machine is _"what the agent says"_ (structured JSONL events). +Terminal capture is _"what the screen shows"_ — the actual text of a pane, +triaged for known-broken patterns. A pane can be `Working` while its screen +reads `Active: failed (Result: exit-code)`. Attention is a view over **both**. + +A `TerminalRecorder` keeps a bounded frame history (`DEFAULT_HISTORY_CAPACITY` += 256 frames). A frame's identity is the **SHA-256 of its stripped text**, so +polling a near-static pane every second collapses into a compact log of actual +state transitions instead of thousands of duplicate frames. `capture_tmux_pane` +shells out to `tmux` for the capture, but `observe()` takes raw text directly — +the dedup and triage logic is fully testable with no terminal attached. + +→ [`crates/colibri-glasspane/src/terminal.rs`](../../crates/colibri-glasspane/src/terminal.rs) +(`TerminalRecorder`, `Observation`, `capture_tmux_pane`) + +### Signature triage, data-driven per OS + +A `SignatureSet` scans stripped terminal text and classifies the screen into +`failures` / `warnings` / `info` / `healthy` (`Severity::{Error, Warn, Info, Ok}`). +Patterns match as case-insensitive substrings; the first hit records a +signature and it is not double-reported. Every match carries a human +`next_action` and an optional `invoke` (a skill the agent can run to +remediate) — a hit is not "something happened" but "here is what it means and +what to do". + +The detection engine is **data-driven**: a FreeBSD host and a Linux host load +different `Signature` sets but share the same matcher. `SignatureSet::linux_default` +ships a small starter set; callers build their own with `SignatureSet::new`. +This is the per-OS knob Colibri's capability routing leans on. + +→ [`crates/colibri-glasspane/src/signatures.rs`](../../crates/colibri-glasspane/src/signatures.rs) +(`SignatureSet`, `Severity`, `Signature`, `Detection::alertable`) + +### Alerts are edge-triggered, not level-triggered + +A failure/warning signature is reported **only on the frame where it first +appears**, not on every subsequent frame that still shows it — returned as +`Observation::Recorded { uuid, new_alerts }` with only the newly-fired matches. +When the condition clears and later recurs, it fires again. Level-triggered +alerts on a 1s poll would re-notify every second for the lifetime of a stuck +pane; edge-triggering makes each alert mean "this just started." + +→ [`crates/colibri-glasspane/src/terminal.rs`](../../crates/colibri-glasspane/src/terminal.rs) +(`TerminalRecorder::observe`, `Observation::new_alerts`) + +## What is still open + +- **Outbound push.** Attention is surfaced on-screen (the bar, the highlight). + The operator supervises headless hosts over Tailscale, not by watching the + TUI. Pushing attention **out** — a desktop notification on the live image + and a Telegram message — is the highest-impact unfinished piece. Token is + already provisioned; transport (`colibri notify` vs. a glasspane event a + harness hook fires) is undecided. +- **Answering a blocked agent from the dashboard.** The snapshot API is + read-heavy by design. A write path ("send input to pane N" over the daemon + socket) would let the operator respond to a `Blocked` pane from the TUI. + Changes the socket from supervision to interactive control — its own design + pass. + +## See also + +- [glasspane](./glasspane.md) — the state machine attention is a view over +- [tui](./tui.md) — the dashboard that surfaces attention +- [terminal](./terminal.md) — the terminal capability attention's keybindings rely on diff --git a/docs/wiki/terminal.md b/docs/wiki/terminal.md new file mode 100644 index 0000000..fefe652 --- /dev/null +++ b/docs/wiki/terminal.md @@ -0,0 +1,101 @@ +# Terminal — capability, not brand + +← [index](./index.md) + +## What this is + +A decision about **which terminal capability** Colibri's operator surfaces +depend on, and why the choice fell on Kitty. The decision is about +_capabilities_ (extended-key reporting); Kitty is the instance that provides +them. A terminal without the capability is not "wrong" — it just degrades +specific keybindings. + +## Decision + +The dashboard client (`colibri-tui`) and the agents it supervises (pi, zot) +are keyboard-driven, and several of their bindings rely on distinguishing +**modified keys**: `Tab` vs `Shift-Tab` (cycle sessions), `n` vs `N` (next vs +previous attention pane), `Enter` (open detail). Terminals built on VTE +(xfce4-terminal, GNOME Terminal, Sakura) and Qt-based Konsole **collapse** +modifiers: `Shift-Enter`, `Ctrl-Enter`, and `Alt-Enter` all arrive as a plain +`Enter`, so two distinct bindings become indistinguishable. + +The recommended terminal is **Kitty**: GPU-accelerated, keyboard-driven, and it +reports modified keys via the Kitty keyboard protocol / extended-keys. It is the +shipped default on the operator USB, with `xterm` retained as the always-works +fallback (Kitty is GPU-only; it cannot start on a headless `bhyve` surface with +no GL, so the rescue path falls back to `xterm`). + +→ [`crates/colibri-glasspane-tui/src/main.rs`](../../crates/colibri-glasspane-tui/src/main.rs) +(the bindings above) + +## Decisions + +### tmux must forward modifiers, not strip them + +Inside tmux the same collapse happens by default: tmux strips modifier +information unless told otherwise. The live USB ships a `~/.config/tmux/tmux.conf` +that enables passthrough: + +``` +set -g extended-keys on +set -g extended-keys-format csi-u +``` + +`csi-u` is the most reliable format and needs tmux 3.5+ (the live USB's port +is 3.5a). Pre-3.5 tmux (e.g. an older Linux build host) omits the second line +and falls back to the xterm `modifyOtherKeys` format, which Colibri also +parses. **Where this matters:** `colibri-tui` launched raw (no tmux) gets +modified keys natively; the tmux config only matters for the run-inside-tmux +workflow. + +→ `crates/colibri-glasspane-tui/src/main.rs` (the event loop's key handling) + +### Raw-kitty vs in-tmux is a real distinction + +Two equally-valid ways to run the dashboard: + +- **Raw** — `kitty /usr/local/bin/colibri-tui`. Kitty reports modifiers + directly; no tmux config needed. This is what the live-USB desktop launcher + does. +- **In tmux** — `kitty` then `tmux` then `colibri-tui`. Now the tmux + extended-keys config above is load-bearing; without it, `Shift-Tab` and `N` + stop reaching the app. + +The desktop launcher path is raw by design, so the dashboard works without any +operator tmux setup. The in-tmux path is for operators who want tabs/splits +around the dashboard. + +### The SSH terminfo gotcha + +Kitty sets `TERM=xterm-kitty`. A remote host that has never seen Kitty does not +carry that terminfo entry, so `tmux a` fails with `missing or unsuitable +terminal: xterm-kitty`. The fix is Kitty's SSH kitten, which copies the terminfo +to the remote on connect — used as the `ssh` alias on operator machines. Lying +with `TERM=xterm-256color` works but discards the extended-key capability, +defeating the reason for the terminal choice. + +### pi surfaces the same requirement + +The pi harness — a spawnable Colibri backend — prints a startup warning when +tmux extended-keys is off, because its own bindings (`Enter` to submit, +`Shift-Enter` for newline) hit the identical collapse. The decision here is the +same one, stated for Colibri's surfaces: pick a terminal that reports +modifiers, and configure tmux to forward them. + +## Requirements, stated once + +- A terminal that reports modified keys (Kitty, Ghostty, WezTerm). On the + operator USB that terminal is Kitty. +- For the in-tmux workflow: tmux 3.5+ with `extended-keys on` + + `extended-keys-format csi-u` (pre-3.5: `extended-keys on` only). +- For SSH into a Kitty session: the Kitty SSH kitten, or a one-time terminfo + install, so `TERM=xterm-kitty` resolves on the remote. + +## See also + +- [tui](./tui.md) — the dashboard whose bindings drive this requirement +- [operator-attention](./operator-attention.md) — the jump keys (`n` / `N`) + that depend on modifier reporting +- [deployment](./deployment.md) — the operator USB that ships Kitty + the tmux + config