ADR-021: Terminal-Native HITL and Agent-Loop Transparency¶
Metadata¶
- Status: Accepted (2026-07-30 — v1 shipped in the TUI: terminal-native approval modal, stall
watchdog,/help+ palette,~/.victor/keybindings.json, Esc interrupt, and an inferred phase
indicator. Exact phase-events (framework enhancement, FEP-gated) and REPL-surface approval parity
remain deferred. Was Proposed.) - Date: 2026-07-29
- Decision Makers: Vijaykumar Singh
- Related ADRs: 020 (interactive TUI — the surface these render on), 001 (service-first runtime —
the loop being surfaced) - Work tracked by: TD-23
- Benchmark: competitive-benchmark-2026-07.md §2, §4
Context¶
Two terminal-first expectations are unmet:
- Approvals are browser-bound. Human-in-the-loop tool approval renders through Chainlit's
AskActionMessage(Approve/Reject buttons) in the web UI. A user invictor chatwho hits a
policyASKverdict onbash/write_file/git_pushhas no in-terminal way to approve — they
must switch to the browser surface. Aider approves with ay/nprompt in the terminal; Claude
Code confirms inline. Victor does not. - The loop is opaque. During execution the REPL shows a generic "running…" spinner. Victor runs
a research-rooted PERCEIVE→PLAN→ACT→EVALUATE→DECIDE loop (framework/agentic_loop.py) and
tracks per-turn cost (C0), but none of that phase/cost state is surfaced inline. Claude Code shows
the active phase and token/cost in its status line.
Two smaller ergonomics gaps compound it: there is no /help overlay for the 26+ slash commands
(discoverability is docs-only), no user keybindings config, and streaming rendering can block
on a stalled agent — the Rich Live context waits on events with no watchdog, so a wedged loop
reads as a frozen terminal (cf. TD-20's wedged-loop failure mode).
Decision¶
Make HITL and loop state legible in the terminal, reusing the framework's existing signals (no
new agentic loop, no new event vocabulary):
- Terminal-native approval. Add a CLI/TUI approval renderer as a peer to the Chainlit one, both
mapping the same framework approval request →ApprovalStatus. In the REPL: an inline prompt with
a preview of the tool call (command / file diff / content) and[a]pprove / [r]eject / [v]iew.
In the TUI (ADR-020): the inline diff pane's approve-in-place control. The Chainlit path stays for
the web surface; the framework approval contract is surface-agnostic. - Phase + cost indicator. Surface the live PERCEIVE→PLAN→ACT→EVALUATE→DECIDE phase and the
running token/cost from the existing per-turn tracker in the status line (TUI sidebar / REPL
status suffix). Read-only projection of state the loop already emits. /helpdiscoverability.slash/handler.pyalready haslist_commands(); wire a/help
command and a first-turn hint. No new command infrastructure.- Keybindings config. Load
~/.victor/keybindings.json(optional) to rebind TUI actions and
slash shortcuts; ship sane defaults so the file is never required. - Stall watchdog. Wrap the streaming event wait with a timeout that flips the indicator to
"agent unresponsive (Ns)" and offers interrupt, instead of a silent freeze. Pairs with the
existing fail-safe approval timeout (120s → reject).
Rationale¶
- First principles. Approval and progress are control-plane interactions; they must live where
the user is (the terminal), not force a context switch. Opacity during long runs erodes trust more
than latency does. - Reuse. Every piece rides existing machinery: the policy engine's ASK verdicts, the surface-
agnostic approval contract, the loop's phase state,list_commands(), the per-turn cost tracker.
This is wiring, not new subsystems. - Co-design. Specified with ADR-020 (its render surface) and consistent with TD-20's lesson that
a wedged loop must be visible and killable, not silent.
Consequences¶
- Positive: terminal users never leave the terminal; runs are legible; wedged loops surface
instead of hang; commands are discoverable. - Negative: two approval renderers to keep in lockstep behind one contract; watchdog timeouts
need tuning to avoid false "unresponsive" on legitimately slow tools. - Neutral: web/Chainlit approval and the HTTP HITL endpoints are unchanged.
Implementation¶
Client-layer; no companion FEP unless the surface-agnostic approval contract needs a new field
on the public victor.framework surface — if so, that field lands via a small FEP first, then this
ADR consumes it.
- Extract the approval-request→status mapping to a surface-agnostic helper; add the CLI/TUI renderer.
- Add the phase/cost status projection (read-only) to REPL + TUI.
/help+ first-turn hint;keybindings.jsonloader + defaults.- Stall watchdog around the streaming wait; wire interrupt.
Alternatives Considered¶
- Keep approvals web-only, document it. Rejected: contradicts the terminal-first category; the
§2 gap is the point. - Auto-approve in CLI to avoid the prompt. Rejected: defeats the policy engine's safety purpose;
the ASK verdict exists precisely to gate destructive tools. - Full progress dashboard in CLI. Deferred: the phase/cost line is the high-value slice; a richer
dashboard is the web/observability surface's job.
References¶
- ADR-020, ADR-005
victor/framework/agentic_loop.py(PPAED loop),victor/ui/slash/handler.py,
victor/ui/rendering/live_renderer.py,victor/ui/chat_app/approval.py(Chainlit path)- TD-20 (wedged-loop / log-flood lesson)
Revision History¶
| Date | Version | Changes | Author |
|---|---|---|---|
| 2026-07-29 | 1.0 | Initial ADR — terminal-native HITL, phase/cost indicators, /help, keybindings, watchdog | Vijaykumar Singh |