main
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e5b8741d2 |
feat(cli): route host-scoped requests to the replica holding the host's tunnel (#4185)
Adds a per-host routing key (the ``X-Databricks-Omnigent-Slice-Key`` header) so that, on a horizontally-scaled multi-tenant deployment, every request scoped to a given host or session lands on the replica that holds that host's runner tunnel: the host's control tunnel, its runners' tunnels, and all of a session's turn/resource/stream traffic converge on one replica when they carry the same key (the host_id). On an unsharded / single-replica deployment the key is never emitted, so this is a no-op there. Client-side only. The key is built centrally in ``cli_auth.databricks_request_headers`` (gated on the workspace-hosted mount) and threaded through the one factory ``open_server_client`` plus ``_remote_headers`` / ``open_daemon_client``. Callers pass a host_id when they have one; runner-side callers (forwarders, permission checks) inherit it automatically from the ``OMNIGENT_RUNNER_SLICE_KEY`` env var the host stamps at runner launch, so no per-callsite change is needed there. The WebSocket attach handshake and its reconnects carry the same key. ``chat._remote_headers`` gains a ``host_id`` keyword (defaulting to ``None`` so probes and health checks are unaffected). ``_DatabricksTokenAuth`` resolves the session's host per request from the session→host map and can be repointed via ``pin_session`` when a client outlives its session (e.g. a ``--fork`` in the REPL lands under a new conversation id on a new host). Session-host state is always written on attach — clearing a stale mapping when the server reports no host matters as much as setting one. A ``tests/cli`` conftest fixture isolates the runner machine's own host identity so "no slice key on this call" assertions are hermetic regardless of whether the box running the suite is itself a host. Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> |
||
|
|
c5659e7c40 |
fix(hermes-native): advance the mirror cursor per row, not per item (#4261)
* fix(hermes-native): advance the mirror cursor per row, not per item One Hermes `messages` row expands to several mirror items sharing a `msg_id` (a reasoning delta, the prose, one `function_call` per tool call), but the forwarder advanced and persisted `last_id = action.msg_id` after each item. When an earlier item of a row delivered and a later one's POST failed, the cursor had already moved past the row, so the next poll's `WHERE id > last_id` skipped it and the undelivered items were lost permanently: a silent, unrecoverable drop of an assistant turn's tool call or prose on any transient post failure mid-row. Advance `last_id` only at a row boundary, marked by the new `_TurnAction.last_of_row`. A row that fails partway records `partial_row_id` / `partial_row_items`, and the retry re-reads that row with its already-delivered prefix dropped. The prefix-drop is required, not defensive: `_post_conversation_item` carries no idempotency key, so re-reading the row without it would mirror the delivered items twice. The partial row is named explicitly rather than implied as "the row after `last_id`", because compaction soft-deletes rows and an implied offset could be applied to the wrong row after the row it describes disappears. The per-poll heartbeat write and the compaction re-pin both carry or clear the new fields, so a later poll cannot silently zero them. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> * fix(hermes-native): restart the in-row item count on a new row The in-row delivered count was only zeroed when a row reached its final item. A row that fails partway can disappear before its retry: compaction soft-deletes it, and the child re-pin that resets these fields is skipped when the session has no child (the code logs "staying on parent"). The stale count then carried into the next row, so that row's retry dropped undelivered items as already delivered, losing them permanently: the same silent loss this cursor exists to prevent. Count from 1 whenever the row is not the one already in progress. Also pass the partial fields explicitly at the child re-pin write (the one write site of four relying on dataclass defaults) so a future default change cannot silently break it. Found by Polly review on #4261. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> --------- Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> |
||
|
|
85740d5f74 |
refactor(native): type read-only SQLite connects (#3759)
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com> |
||
|
|
7048f7a38b |
fix(hermes): introspect state.db schema to survive cross-version column drift (#2774)
Signed-off-by: David O'Keeffe <dgokeeffe@users.noreply.github.com> Co-authored-by: David O'Keeffe <dgokeeffe@users.noreply.github.com> |
||
|
|
78b37f20de |
fix(runner): resolve and re-materialize file attachments on remote-runner history reload (#2085)
* fix(runner): resolve and re-materialize file attachments on remote-runner history reload Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com> * fix(runner): seed the native-session compaction anchor; tolerate malformed file metadata Native-harness sessions skip the history reload entirely, which also skipped seeding the last server item ID that harness compaction persistence anchors on — compactions then silently stopped persisting. Session create now fetches just the newest item ID (newest-first, single item, no attachment downloads) for native harnesses. A 200 metadata response with an unparseable body no longer aborts attachment resolution: both resolvers (the runner's message-content resolver and the claude-native transcript rebuild) fall back to the content response's Content-Type for the media-type hint. Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com> * refactor(attachments): centralize file_id resolution and reference-line emission in native_attachments The transcript rebuild and the runner each carried a full copy of the file_id fetch-and-inline pipeline, and nine native executors repeated the same materialize-or-marker block. Both now live in native_attachments: resolve_file_id_block() serves the runner and the transcript rebuild, attachment_reference_line() serves the executors, and ATTACHMENT_MARKER_STRIP_PATTERN replaces four hand-copied forwarder regexes. Materialized filenames are sanitized the same way as marker names so a bracketed filename cannot break the marker consumers, and the resume dedupe short-circuits on file size before reading bytes. Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com> * fix(attachments): replay resolved history attachments as structured content Cold-started claude-sdk sessions flattened prior turns into a text prefix, so a resolved historical image reached the model as the marker [image: name, media_type, N base64 chars]. The bytes never arrived, which leaves the #882 symptom in place for that harness: the model describes an attachment it cannot see. Prior-turn attachments now replay as real Anthropic image/document blocks via the existing converter, interleaved in transcript order. Text-only history still takes the plain-string path and renders byte-identically, unresolved attachments keep their existing marker, and base64 still never enters prompt text. Materialization also derives its collision suffix from a content hash rather than a random one, so a history carrying two distinct uploads of the same filename keeps one file per payload instead of gaining a copy on every transcript rebuild. The two tests that asserted the compact-placeholder shape are replaced by cold-reload tests: that shape is the behavior being corrected, but the invariant those tests protected (no base64 in prompt text) is asserted against the prompt's text blocks. Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com> * refactor(attachments): collapse duplicated prompt-shape branches The structured and plain-text arms of _build_prompt returned the same value whenever the latest message was multimodal, and re-scanned the block list to decide which arm to take. Coalescing already leaves an all-text history as one block, so the block count answers that. Materialization's second identity check was a no-op guarding a write that produces the same bytes, so the collision path flattens to one branch. Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com> * refactor(tests): keep runner conftest identical to upstream Move the file-server fake's items/failure/malformed-meta behaviors out of the shared _FakeFileServerClient into local subclasses in the one file that uses them, so conftest.py stays in sync with upstream and per-test modes stay next to their tests. Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com> --------- Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com> Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com> |
||
|
|
2561d54ee5 |
fix(hermes): mirror native reasoning to the web conversation (#1645)
The hermes-native forwarder's messages SELECT omitted the reasoning columns Hermes persists, so thinking shown in the TUI never reached the web conversation. Read reasoning_content/reasoning and emit a one-shot external_output_reasoning_delta before the assistant message (started=True), matching the codex- and opencode-native transient reasoning contract. The structured codex_reasoning_items column is left alone. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> |
||
|
|
3ff33e645f |
fix(hermes-native): render tool-call cards with a live spinner (#2046)
* feat(hermes-native): live tool-call cards via a per-turn response_id
hermes-native chat rendered tool-call cards as static/completed instead of live
(spinner + ticking timer). The web keys a live card off a running/waiting
session.status edge whose response_id matches the mirrored function_call items'
response_id — but the hermes forwarder stamped a per-row id (hermes:{msg_id}) and
never posted a running edge (running/idle came only from the runner's id-less
PTY-activity watcher).
Assign one response_id per turn (hermes_turn_{opening-msg-id}) shared across the
turn's rows, POST a running edge carrying it at turn start, and stamp the turn's
function_call items with the same id (_annotate_turn_actions). The per-turn id is
persisted in _ForwardState so a turn spanning polls / a restart keeps it. The
running post is best-effort — a failed live-card edge never aborts mirroring.
Deliberately keep idle ownership with the existing completed-turn post and the PTY
watcher (the server pops the active response id on any idle), so an aborted turn
whose terminal row is never written still resolves the card — no watchdog needed.
Discovery always starts turn tracking fresh, so a claim-yield / compaction re-pin
reacquire never resurrects a stale turn id.
Closes #1874
Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
* fix(hermes-native): render tool-call cards with a live spinner
Four forwarder changes so a hermes-native tool call shows a live spinner
plus ticking timer while it runs (on the first turn too):
- Carry the turn's response_id on the completed-turn idle post so the web
settles that exact card. An id-less idle is a no-op on the web while a
response is still streaming, so the card never resolved deterministically.
- Re-assert the running edge (with the turn id) on each poll while a turn is
in flight. The runner's PTY-activity watcher emits an id-less idle after
~1s of pane quiescence (a silent tool such as sleep), which pops the turn's
active response server-side; re-asserting keeps it live until the turn ends.
The running edge mirrors no message row, so it does NOT advance the last_id
cursor — only the item POST does, and only after it succeeds — so a crash
between the two re-reads the opening row on restart instead of dropping it.
- Emit an assistant row's prose BEFORE its function_calls. The text is the
model's preamble that precedes the calls, and it keeps the in-flight tool as
the trailing item so the web renders its live spinner (a trailing message
would otherwise leave the tool static until its output landed).
- Close the turn on an empty-prose assistant terminal row. Such a row yields a
role-less sentinel, so carry the row role on the sentinel and read it in turn
detection — otherwise the turn's id never clears, the running re-assert loops
forever, and the web card is stranded live.
Adds forwarder tests for the per-turn id across parallel/sequential tool calls,
the running re-assert, its cursor-safety, preamble-before-tool_calls ordering,
and empty-prose terminal turn-closing, plus a web render test for multi-call
turns.
Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
* docs(hermes-native): reconcile the abort story with the running re-assert
The module and _annotate_turn_actions docstrings claimed the PTY-activity
watcher's idle 'remains the abort-robust resolver', but the per-poll running
re-assert re-arms the turn id inside the watcher's ~1s quiescence window. An
aborted turn whose terminal row is never written is indistinguishable from a
silent tool in the store, so its card stays live until a terminal row lands
(an interrupt's empty-prose row closes the turn) or the next user turn
re-opens with a fresh id. State that trade-off explicitly and name it in the
re-assert test.
Co-authored-by: Isaac
---------
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
||
|
|
c49cd59692 |
fix(hermes): bound the idle turn count to the mirrored high-water mark (#2161)
A final assistant row that lands while a poll's batch is still being POSTed was picked up by the fresh completed-turn count at the end of the same iteration, ringing the parent-waking idle edge before the row itself was mirrored — a sub-agent orchestrator woke to a transcript missing the final answer. Count only rows at or below the mirror's high-water mark so the completion signal can never overtake the content it announces. Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ce225f3117 |
feat(polly): add cursor and hermes coding sub-agents (#1844)
* feat(polly): add cursor and hermes coding sub-agents
Adds `cursor` (cursor-native) and `hermes` (hermes-native) to the polly
orchestrator, taking the roster to six: claude_code, codex, opencode, cursor,
hermes, pi. Both are native terminal harnesses (openable / take-over-able in the
Subagents panel), widening cross-vendor review.
- examples/polly/agents/{cursor,hermes}/config.yaml (new): standard implement /
review / explore contract and blast_radius(gate_pushes=false), matching the
peers.
- examples/polly/config.yaml: roster is now six; preflight checks `cursor-agent`
and `hermes`; tools.agents, routing, cancellation notes, and comments updated;
spawn_bounds.max_dispatches_per_turn 5 -> 6 so one fan-out round can launch
every worker.
- examples/polly/skills/{investigate,fanout,cross-review}: cursor and hermes
wired in as full peers (implementer, reviewer rotation, explore lens).
- tests: roster list, per-worker loops, vendor count (4 -> 6), policy count
(7 -> 9), the shipped-bundle declared set, and the brain-override
worker-harness map updated for the two new workers.
The parent-wake plumbing that makes cursor/hermes usable as headless polly
workers lands in the following commit.
* fix(native): wake parent orchestrator when cursor/hermes finish a turn
cursor-native and hermes-native only emitted the PTY watcher's web-spinner
`session.status: idle` edge, which never wakes a parent orchestrator — so as
polly sub-agents they finished silently while claude/codex/opencode/pi woke the
parent via an `external_session_status: idle` POST. Both now post that event
once per completed turn, deduped against a persisted posted-count and
restart-safe.
cursor: the stop hook records a turn-end marker (cursor_native_status); the
forwarder tails it and posts idle. hermes (no stop hook) derives turn-end from
state.db — an assistant row with no tool_calls is the agentic loop's terminal
step. The runner clears the new poster state on terminal recreation so a stale
count can't skip or re-fire the wake.
Ported from the original cursor/hermes/opencode roster work; without it the two
new polly workers added in the previous commit would dispatch and never notify
polly on completion.
* feat(web): give Hermes its own glyph in the Subagents panel
Hermes rendered with the generic omnigent fallback icon because there was no
HermesIcon component and neither icon resolver had a `hermes` case — even though
`iconKind: "hermes"` was already declared on the native-agent spec. Add an
original caduceus glyph (currentColor, matching its sibling icons) and wire it
into AgentCard.getAgentIcon and SubagentsPanel.brandChildIcon so the hermes
polly sub-agent shows its own icon like the other native harnesses.
* style(web): prettier-format HermesIcon path strings
prettier collapses the two split path-string literals onto single lines
(they fit the print width); match it so format:check passes.
* fix(hermes-native): rebase idle posted-count on compaction re-pin
The completed-turn count is keyed per hermes_session_id, but the idle dedup
baseline (posted_count) is per bridge dir. On an in-session compaction the
forwarder re-pins to the forked child (new session_id, count restarts near 0)
without touching posted_count, so the guard completed_turns > posted_count
stayed False until the child exceeded the parent total — suppressing the
child session's early idle posts and hanging a headless polly worker that
compacts mid-task then finishes. Rebase posted_count to the child's current
count on re-pin (where last_id is reset to 0). Adds a regression test that
fails without the rebase, and corrects the clear_hermes_status_state docstring
(count is per hermes_session_id, not per terminal).
Flagged by the Polly AI review on #1844.
* chore(native): drop unused _logger from cursor/hermes status modules
Neither cursor_native_status nor hermes_native_status logs anything; the
_logger = logging.getLogger(__name__) definition and its import logging were
dead (flagged by github-code-quality). Remove both. No behavior change.
* docs(cursor-native): note idle block runs outside the store-gated branch
The cursor idle-post block sits at the poll-loop body level, deliberately
outside the if store_path mirroring branch, so a stop-hook turn-end marker
is picked up even on a poll where the SQLite store is unbound or empty.
Make that placement explicit (per PR review). Comment-only.
|
||
|
|
0ca8f06894 |
fix(hermes): re-pin to the child session after auto-compression (#1646)
The hermes-native forwarder pinned one hermes_session_id for life. On auto-compression Hermes ends that session and creates a child (sessions.parent_session_id chain), so the forwarder kept polling the dead parent and the web conversation went silent mid-run. When compaction is detected, discover the newest child via parent_session_id and re-pin to it (reset last_id and re-PATCH external_session_id), staying on the parent when there is no child. Forwarder-only: it reads Hermes' live state.db, which carries parent_session_id. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> |
||
|
|
67c26ad30e |
feat: persist compaction items for native harnesses (cursor, codex, hermes) (#1331)
* feat: persist compaction items for native harnesses (claude, cursor, codex) When native harnesses compact their context, persist a compaction boundary item to the conversation store so transcript rebuild from DB knows where compaction happened. Also update compaction_to_history_items to use compacted_messages when available. - claude-native: reads post-compaction messages via get_session_messages() - cursor-native: reads post-compaction messages from SQLite store - codex-native: persists boundary marker (no compacted_messages available) - compaction.py: compaction_to_history_items uses compacted_messages Co-authored-by: Isaac * test: add unit tests for native compaction item persistence Cover _persist_native_compaction_item (cursor) and _persist_codex_compaction_item (codex) — verifying POST shape, last_item_id resolution, compacted_messages inclusion/omission, and the empty-items fallback path. Co-authored-by: Isaac * fix: add idempotency guard for codex compaction item persist Both _handle_completed_item (contextCompaction) and _maybe_handle_turn_event (thread/compacted) can fire for the same compaction boundary, causing duplicate persist calls. Add a compaction_item_persisted boolean to _CodexForwarderState that gates the persist and resets when a new compaction starts (in_progress), mirroring the existing compaction_status_posted dedup pattern. Co-authored-by: Isaac * fix(ci): sort imports in test_codex_native_forwarder Co-authored-by: Isaac * feat(codex-native): include compacted_messages from server items Read all persisted conversation items from the server and include them as compacted_messages in the compaction event. This enables transcript rebuild from DB to replay the full post-compaction state. Co-authored-by: Isaac * fix(codex): revert compacted_messages — server items are pre-compaction The server's mirrored items are the pre-compaction history, not the post-compaction state. Storing them as compacted_messages would replay the full uncompacted history on resume, defeating the purpose. Codex's post-compaction state is internal to its app-server protocol and not readable from the forwarder, so the boundary marker (last_item_id) is the only durable signal. The synthetic summary pair fallback handles resume. Co-authored-by: Isaac * feat(hermes-native): truncate long tool outputs in web UI mirror Skill loads and other verbose tool results no longer flood the chat view. Outputs over 1000 chars are truncated with a "… (truncated)" marker. The full output remains visible in the embedded terminal. Co-authored-by: Isaac * Revert "feat(hermes-native): truncate long tool outputs in web UI mirror" This reverts commit |
||
|
|
98c5e350de |
feat(hermes-native): support resume via --resume (#1377)
* feat(hermes-native): add fork/resume support via external_session_id PATCH and --resume flag The hermes-native forwarder now PATCHes external_session_id to the Omnigent server when it first discovers the Hermes session, enabling fork workflows. The terminal launcher passes --resume to Hermes when forking with history so the TUI loads the prior conversation context. Co-authored-by: Isaac * fix: add hermes-native to _FORK_HISTORY_NATIVE_HARNESSES Without this, fork labels (FORK_CARRY_HISTORY, FORK_SOURCE_EXTERNAL_SESSION) are never stamped on hermes-native forks, so --resume is never appended. Co-authored-by: Isaac |
||
|
|
365988df25 |
feat(hermes-native): truncate long tool outputs in web UI mirror (#1356)
* feat(hermes-native): truncate long tool outputs in web UI mirror Skill loads and other verbose tool results no longer flood the chat view. Outputs over 1000 chars are truncated with a "… (truncated)" marker. The full output remains visible in the embedded terminal. Co-authored-by: Isaac * feat(hermes-native): replace skill-injected user messages with /name Hermes injects skill content as a user message with the full prompt. Detect these by the "[IMPORTANT: The user has invoked..." prefix and replace with a short "/skill-name" summary in the web UI mirror. Co-authored-by: Isaac * refactor: remove truncation helper, keep skill-name replacement only Co-authored-by: Isaac |
||
|
|
37043a837b |
feat(hermes-native): add Omnigent policy enforcement, cost tracking, and interrupt (#1248)
* feat(hermes-native): add policy hook support, cost tracking, and interrupt Wire Omnigent policy enforcement into the hermes-native harness by writing a per-session HERMES_HOME with a pre_tool_call shell hook (reusing the existing hermes_policy_hook.py). Add a _HermesUsageTracker that posts the model name via external_session_usage events in the forwarder poll loop. Add interrupt_session() to HermesNativeExecutor via inject_interrupt(). Co-authored-by: Isaac * feat(hermes-native): add compaction via /compress slash command Hermes CLI supports /compress to compact conversation context. Add inject_compress_command() to the bridge and wire a compact handler in the runner that injects /compress into the TUI pane — same pattern as claude-native's /compact and codex-native's /compact. Co-authored-by: Isaac * feat(hermes-native): register Omnigent MCP server in per-session config Add mcp_servers.omnigent to the per-session HERMES_HOME config.yaml, pointing to the same serve-mcp stdio bridge that claude-native and codex-native use. This exposes Omnigent builtin tools (sys_session_*, sys_agent_*, load_skill, web_fetch, etc.) to the Hermes model. Also writes bridge.json with an auth token for serve-mcp, mirroring codex_native_bridge.write_mcp_bridge_config(). Co-authored-by: Isaac * style: fix ruff format and lint issues Co-authored-by: Isaac * fix(hermes-native): point forwarder at per-session state.db When HERMES_HOME is set to a per-session dir (for policy hooks / MCP), Hermes writes state.db there instead of ~/.hermes. The forwarder was still reading the default ~/.hermes/state.db and never finding the session's messages. Co-authored-by: Isaac * fix(hermes-native): use Ctrl+C instead of Escape for interrupt Hermes uses Ctrl+C to interrupt a running turn, not Escape. Double-press within 2s forces exit. Co-authored-by: Isaac * fix(test): update interrupt test to expect C-c instead of Escape Co-authored-by: Isaac * fix(hermes-native): add hermes-native bridge root to serve-mcp trusted list serve-mcp rejected hermes-native bridge dirs because they weren't under a known bridge root. Add hermes_native_bridge.bridge_root() to the trusted parent list in _trusted_parent_for_bridge_dir(). Co-authored-by: Isaac * feat(hermes-native): mirror tool calls as function_call events in web UI Read tool_calls, tool_call_id, and tool_name columns from Hermes' state.db. Assistant rows with tool_calls JSON emit function_call items; tool-role rows emit function_call_output items. This makes tool calls visible as structured events in the web UI instead of being silently skipped. Co-authored-by: Isaac * style: fix ruff format in forwarder test Co-authored-by: Isaac * style: fix line length in forwarder test Co-authored-by: Isaac |
||
|
|
edbdca8c0e |
feat(native): Hermes native TUI harness + synced web approval for hermes-native & goose-native (#1163)
* feat(hermes): add native Hermes TUI harness (hermes-native) Adds `hermes-native`, the native counterpart to the headless `hermes` harness (#1132), following the goose-native pattern: `omnigent hermes` launches the real `hermes` prompt_toolkit TUI in a runner-owned tmux pane, the harness executor injects each web turn via tmux bracketed paste, and a forwarder tails Hermes' SQLite `state.db` to mirror the transcript back into the Omnigent chat view. Unlike goose-native, Hermes auto-generates its session id (no `--name`), so the forwarder discovers the session cursor-native style: newest `sessions` row whose `cwd` matches the workspace and `started_at` is at/after the launch floor, with a claim guard for concurrent same-cwd sessions. Like goose-native it applies no Omnigent policy hooks — the TUI's own approval prompts gate tools, using the user's own `~/.hermes` config. New modules: hermes_native.py (CLI), hermes_native_bridge.py (tmux inject), hermes_native_forwarder.py (state.db mirror), inner/hermes_native_executor.py + hermes_native_harness.py. Wires the harness registry, aliases, native-coding-agent metadata, runner terminal spawn/interrupt/stop, CLI subcommand, resume dispatch, onboarding readiness, and the ap-web frontend entry. Adds unit tests for the executor, CLI/wiring, and forwarder (discovery, claim guard, mirroring). Co-authored-by: Isaac * fix(ap-web): add "hermes" to ConversationIconKind so the web UI builds getConversationIconKind returns a native agent's iconKind (now including "hermes") as a ConversationIconKind; the union was missing "hermes", so `tsc -b` failed (TS2322) and broke `omnigent[all]` install (web UI build). Mirrors how "qwen" — also glyph-less — is listed in both unions. Co-authored-by: Isaac * fix(hermes-native): render as a native terminal + keep the gold TUI colors Two fixes from live testing: - Add `terminal_hermes_main` to ap-web's AGENT_TERMINAL_IDS so isAgentTerminalKey recognizes the hermes pane as the agent terminal; without it isShellView treated it as a plain shell (and it leaked into the Shells inventory) — the same regression pi/cursor/goose/qwen each hit. Adds the matching test. - Drop the NO_COLOR=1 env on the hermes terminal: it disabled Hermes' themed TUI (gold prompt rendered white). The bridge captures the pane with `capture-pane -p` (ANSI stripped) and the forwarder reads SQLite, so color never interferes with scraping. Co-authored-by: Isaac * feat(hermes-native): route tool calls through Omnigent policy (web approval) The native Hermes TUI now gates tools via Omnigent's approval flow, matching claude-/codex-native. The runner builds a per-session HERMES_HOME (the user's full ~/.hermes config copied in, minus state.db, + Omnigent's pre_tool_call shell hook layered on) and launches the TUI with HERMES_HOME=<dir> and HERMES_YOLO_MODE=1. The hook calls the server's policy evaluate endpoint, which parks on an ASK policy until the human responds to the web approval card; YOLO suppresses Hermes' own in-TUI prompt so the web card is the sole gate (the hook fires before, and independent of, Hermes' approval check per model_tools.py). The forwarder tails the per-session HERMES_HOME/state.db. Adds a unit test. Co-authored-by: Isaac * feat(goose-native): route tool calls through Omnigent policy (web approval) The native Goose TUI now gates tools via Omnigent's approval flow. The runner builds a per-session GOOSE_PATH_ROOT holding an Open-Plugins `omnigent-policy` plugin whose PreToolUse hook calls the server's policy evaluate endpoint (which parks on ASK until the human answers the web approval card). Goose's PreToolUse hook fires independent of GOOSE_MODE and denies on `{"decision":"block"}` — the same contract as the hermes hook. GOOSE_PATH_ROOT relocates all of Goose's dirs, so we symlink the real config/data/state back in (preserving the user's auth + the sessions.db the forwarder tails); the plugin lives only under the per-session root, so standalone `goose` never sees it. The hook reads its per-session _OMNIGENT_* values from the terminal env (Goose inherits env into hooks; verified no env_clear), failing open when unset. GOOSE_MODE=auto suppresses Goose's own in-TUI prompt so the web card is the sole gate. Real dirs are resolved by parsing `goose info` (ANSI- and space-tolerant); if they can't be parsed we launch without gating rather than break auth. Adds unit tests for the parser and plugin builder. Co-authored-by: Isaac * feat(policies): ask_on_os_tools recognizes Goose native tools Goose namespaces its built-in developer tools as developer__shell / developer__write / developer__edit / developer__text_editor / etc. Add them to ask_on_os_tools so the standard approval policy gates a native goose session's shell/file tools (web approval card) — without this the policy silently no-ops for goose-native. Adds parametrized coverage mirroring the pi/hermes cases. Co-authored-by: Isaac * fix(native): restore vendors' in-TUI approval (drop YOLO/auto + policy-hook gating) The policy-hook approach suppressed each vendor's own tool-approval prompt (HERMES_YOLO_MODE=1 / GOOSE_MODE=auto) so only a web card gated — which meant approvals showed only in the web chat, never in the TUI, and Hermes ran on YOLO. That's the wrong model for native TUIs. Revert the runner wiring to vendor-native approval: no HERMES_HOME/YOLO (Hermes uses ~/.hermes and its own approval prompt; forwarder tails ~/.hermes/state.db), and GOOSE_MODE=smart_approve so Goose prompts in its TUI. The prompt now appears in the terminal AND the web's embedded terminal pane (answerable from either). This is also step 1 of the chosen cursor-native-style synced mirror; step 2 (a web elicitation card mirrored from the TUI prompt) lands next. The per-session HERMES_HOME / GOOSE_PATH_ROOT policy-hook helpers are left in the tree, unused, pending that follow-up. Co-authored-by: Isaac * feat(native): synced web approval mirror for hermes-native & goose-native Surfaces each vendor's in-TUI approval prompt as a web elicitation card, synced both ways (answer in the terminal OR the web card) — the cursor-native pattern, now for Hermes and Goose. The vendor's own prompt stays the source of truth and the fallback; nothing is suppressed. - Generic POST /sessions/{id}/hooks/native-permission-request route: parks for the web verdict and labels the card per-vendor (agent/policy_name from body). - hermes_native_permissions.py: detects Hermes' `DANGEROUS COMMAND` / `Choice [o/s/a/D]:` block (confirmed against hermes-agent locales/en.yaml by running it from source), sends `o` (approve) / `d` (deny). - goose_native_permissions.py: detects Goose's cliclack `do you allow?` + Allow/Deny radio (from goose-cli prompt_tool_confirmation) and DRIVES the selector — `Enter` for the default Allow, `Down`×N + `Enter` for Deny (N=2 with "Always Allow", else 1). - capture_/send_*_pane helpers on both bridges; both mirrors run alongside the transcript forwarder under one supervised runner task (like cursor). The goose arrow-select driving is position-dependent and the one part worth confirming against a live Goose. Adds parser unit tests for both. Co-authored-by: Isaac * chore(native): drop the reverted policy-hook code, superseded by the mirror The earlier policy-hook elicitation approach (per-session HERMES_HOME and GOOSE_PATH_ROOT plugin) was reverted in favour of the cursor-native-style synced approval mirror, leaving its builders dead. Remove them: delete inner/goose_native_hook.py, drop setup_hermes_native_home / setup_goose_native_plugin_root / real_goose_dirs and their now-unused imports from the bridges (keeping the capture_/send_*_pane helpers the mirror uses), and remove the corresponding tests. Keep ask_on_os_tools' Goose tool-name coverage (useful for any policy that gates goose tools) and the headless harness's hermes_policy_hook.py (still used by `harness: hermes`). Co-authored-by: Isaac * fix(native): correct hermes approval detection + stop goose card pile-up Two live bugs in the approval mirrors: - goose cards piled up and re-appeared at the end: dedup keyed on a hash of the scraped tool context above the cliclack widget, which jitters every poll, so a new card parked each 0.3s and only the latest cleared on a TUI answer. Switch both mirrors to presence-edge: one card per visible-prompt episode (a per- session counter id), cleared on the falling edge. - hermes elicitation never fired: the interactive TUI renders the gate as a prompt_toolkit PANEL titled "⚠️ Dangerous Command" with NUMBERED choices (1. Allow once … 4. Deny), not the legacy `Choice [o/s/a/D]:` input() prompt (fail-closed under prompt_toolkit) that the parser keyed on. Rewrite the parser to detect the panel + read each choice's digit from the panel, and answer with that digit (Hermes' number-key binding selects AND confirms). Robust to the permanent-allowlist option (Deny is 4 with it, 3 without). Confirmed the panel/keys against hermes-agent cli.py by reading it; the goose arrow-select driving and these pane formats still want a live confirm. Tests updated to the real formats. Co-authored-by: Isaac * test(e2e_ui): add native Hermes render-parity suite (satisfies E2E UI gate) Mirrors test_native_goose_render_parity for hermes-native: composer→TUI parity, a TUI-originated turn surfacing in the web UI, and no duplicate rendering, plus a native_hermes_session fixture. Skips when hermes/tmux/config are absent (CI provisions no Hermes account), like the goose/cursor suites. Covers the ap-web Hermes native-agent UI behavior the E2E UI Required gate flagged. Co-authored-by: Isaac * chore(openapi): regenerate openapi.json for native-permission-request route The new POST /sessions/{id}/hooks/native-permission-request route made the checked-in openapi.json stale, failing the Pytest (server-rest) drift test. Regenerated via scripts/dump_openapi.py. Co-authored-by: Isaac * test(native): cover the bridges, approval mirrors, forwarder loop, and CLI helpers The new native modules dropped total coverage below baseline (Coverage gate), and the e2e suites that would exercise them skip in CI (no vendor binaries). Add unit tests: tmux bridge (inject/capture/send/spawn-env, mocked tmux); both approval mirrors (_run_one_approval keystrokes, external_elicitation_resolved, one-card-per-episode supervise); the hermes forwarder loop (discover→mirror) + _post_conversation_item; and hermes_native CLI/daemon helpers (spec, payload decode, tmux-availability, daemon-flow HTTP via a fake client). Lifts the new modules from ~46% to ~70-85%. Co-authored-by: Isaac * test(e2e): exclude hermes-native from the live no-AGENT harness matrix Registering hermes-native broke test_run_harness_live_matrix_covers_registered_ coding_harnesses (it asserts the matrix covers every registered harness). hermes-native is a terminal-first TUI launched via `omni hermes` (tmux pane + bridge), not `omnigent run --harness hermes-native`, and wraps the hermes CLI — so it's excluded like goose-native/qwen-native/antigravity-native. Its coverage is the dedicated hermes-native unit tests. Co-authored-by: Isaac |