6f0257dbc7ae7993339046b56c5665b86b22ab89
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6f0257dbc7 |
feat(kiro): add native CLI harness (#899)
* feat: add Kiro native CLI harness Signed-off-by: Michael Gardner <gardnmi@gmail.com> * fix(kiro): avoid ambient env in tmux attach Signed-off-by: Michael Gardner <gardnmi@users.noreply.github.com> * fix: restore uv.lock pypi.org sources (drop accidental databricks-proxy re-lock) A local `uv run` during the merge re-locked uv.lock against this machine's Databricks-internal pypi proxy, flipping every package source URL. Kiro changes no dependencies and pyproject.toml is unchanged vs main, so restore main's uv.lock verbatim (pypi.org sources). Only registry URLs differed — no version or hash changes. Co-authored-by: Isaac * test(e2e-ui): add native-kiro render-parity suite (E2E UI Required gate) The E2E UI Required gate flagged that #899 changes the agent-picker/session UI (adds Kiro) without a tests/e2e_ui/** test. Add test_native_kiro_render_parity.py mirroring the cursor/goose siblings — composer-IN parity, a TUI-originated turn surfacing OUT, and no duplicate rendering — plus the native_kiro_session fixture. Skip-gated on kiro-cli + tmux, so it skips in CI (no Kiro account provisioned) exactly like the goose/cursor suites, and runs for real where Kiro is signed in. Verified: collects + skips cleanly (kiro-cli absent); ruff clean. Co-authored-by: Isaac * fix: restore ap-web/package-lock.json npmjs.org sources (drop databricks npm-proxy) Same root cause as the uv.lock fix: an npm command during round-1 merge re-resolved one dependency (yaml-1.10.3) against this machine's Databricks-internal npm proxy (npm-proxy.cloud.databricks.com), which CI (pinned to registry.npmjs.org) can't reach -> 'npm ci' ETIMEDOUT. ap-web/package.json is unchanged vs main and Kiro adds no npm dependency, so restore main's package-lock.json verbatim (clean npmjs.org sources). Co-authored-by: Isaac * test(e2e): exclude kiro-native from the live-harness matrix coverage check test_run_harness_live_matrix_covers_registered_coding_harnesses asserts every registered coding harness is either in the live no-AGENT e2e matrix or explicitly excluded. kiro-native is a terminal-first TUI launched via `omni kiro` (tmux pane + bridge dir), not `omnigent run --harness kiro-native`, so — like goose-native / qwen-native / cursor-native — it can't run in this matrix. Add it to the exclusion set with the matching rationale; its coverage is the kiro-native bridge/executor/ forwarder unit tests + the test_native_kiro_render_parity e2e_ui suite. Co-authored-by: Isaac * test(ap-web): set isNativeWrapper in /compact composer menu tests #1139 gated "/compact" behind isNativeWrapper (hidden for non-native harnesses), but the three slash-menu-UX tests that assert "/compact" tops/appears in the suggestions still rendered a non-native composer, so they now fail on main (and on every PR that merges main). Render those three with isNativeWrapper:true so "/compact" is offered, restoring the built-in ordering the tests pin. Test-only; no behavior change. Fixes the inherited ChatPage.composer.test.tsx red on this PR. Co-authored-by: Isaac * test(kiro): cover kiro_native launcher helpers (raise coverage 43%→70%) The kiro-native launcher (omnigent/kiro_native.py) was the largest coverage gap on this PR: its CLI/daemon orchestration is only exercised by the live render-parity e2e, which skips in CI when kiro-cli is absent. Add focused unit tests (with a fake httpx client) for the unit-testable surface: executable resolution, launch-argv assembly, terminal-payload decoding, tmux attach gating, startup-progress forwarding, preflight, resume-id resolution, and the create/fetch/ ensure/find/wait session helpers (success + error branches). Lifts kiro_native.py from 43% to 70%; remaining misses are the daemon-driven async orchestration covered by runner/e2e paths. Co-authored-by: Isaac * test(kiro): rename test env var to avoid exfil-scan false positive The CI exfil scanner flags any added file containing a secret-named source (regex `[A-Z0-9]+_SECRET\b`) together with a network sink. The tmux-allowlist test used `OMNIGENT_SECRET` purely as a non-allowlisted sample var, which matched the secret regex and — combined with the fake httpx client's .post()/.get() in the same file — tripped the "secret-named source + network sink" block. Rename it to a neutral `OMNIGENT_UNLISTED_VAR`; the test's intent (filtering non-allowlisted keys) is unchanged. Co-authored-by: Isaac --------- Signed-off-by: Michael Gardner <gardnmi@gmail.com> Signed-off-by: Michael Gardner <gardnmi@users.noreply.github.com> Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com> |
||
|
|
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 |
||
|
|
da05b924f3 |
feat: Antigravity harness (SDK + native agy CLI) at parity with claude/codex (#892)
* build(antigravity): add google-antigravity SDK dep + host image (agy CLI, lsof, procps) The antigravity SDK harness needs the google-antigravity package; the managed host image needs the agy CLI on PATH plus lsof/procps for the executor's process discovery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity): onboarding — agy auth, harness install/readiness, Gemini provider config Detects/installs the agy CLI, recognizes the Gemini provider family + GEMINI_API_KEY, and wires antigravity into the model catalog, override resolution, and effort levels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): native agy harness — registration, bridge state, launch + TUI delivery Registers the antigravity-native harness (aliases, wrapper labels, resume dispatch), the launch config, and the per-conversation bridge state. The bridge also carries the tmux send-keys delivery (inject_user_message_via_tui) used to type web turns into the agy TUI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): transcript forwarder (read path) + connect-RPC discovery Mirrors agy's JSONL transcript into the Omnigent session (with post-hoc policy audit), and discovers agy's connect-RPC port by conversation-ownership probe so the forwarder can bind the right brain dir. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): TUI web-turn executor + runner/runtime/server wiring The executor types every web turn into the agy TUI (a connect-RPC SendAgentMessage is logged as a SYSTEM_MESSAGE the forwarder would not mirror), and the runner auto-creates the agy terminal + forwarder, advertising its tmux pane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity): ap-web — agent card, new-chat flow, native-agent wiring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(antigravity): e2e-ui new-chat picker shows Antigravity + terminal labels Adds the tests/e2e_ui gate test for the ap-web changes: stubs /v1/agents with the native Antigravity agent, opens the new-chat composer, asserts the agent chip renders the harness-derived label 'Antigravity' (not the raw 'antigravity-native-ui'), and that send POSTs the terminal-first wrapper labels (omnigent.ui=terminal, omnigent.wrapper=antigravity-native-ui). Mirrors the pi-native picker test; runs against a no-agent server (agent-independent UI behavior). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): use os.environ.copy() to clear exfil scanner The Security Scan's exfil-scan.py flags `dict(os.environ)` in added lines as a wholesale-environ-dump shape (regex `(json.dumps|dict|str|repr)\(\s* os.environ`). The direct-tmux-attach helper only copies the environment to drop TMUX before exec'ing `tmux attach` -- a legitimate subprocess-env build, byte-identical to the sibling claude/pi native harnesses, not an exfil. Switch to the idiomatic `os.environ.copy()` (already used in omnigent/onboarding/sandboxes/bootstrap.py), which returns the same dict[str, str] snapshot and is not matched by the heuristic. No behavior change; unblocks Security Scan and the 7 cascading Security Gate checks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): make launch tests hermetic (stub agy binary) The four `test_launch_and_record_*` tests drove `_launch_and_record` → `build_agy_launch`, which uses `agy_binary_path()` as argv[0] unconditionally and raises `RuntimeError` when agy is absent from PATH — true in CI. They only passed locally because agy happens to be installed. One test tried to patch `_mod.agy_binary_path`, but `build_agy_launch` resolves the name in its OWN module (`antigravity_native_launch`), so that patch was ineffective. Add an autouse fixture that stubs `agy_binary_path` at both lookup sites (launch module + the antigravity_native re-export), and drop the ineffective per-test patch. Proven via a no-agy reproduction: the real resolver raises, the tests fail without the fixture and pass with it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(onboarding): keep gemini out of the openai-family "Other provider" picker Adding the `gemini` catalog provider (for the antigravity SDK flavor) put it in `key_providers()` but not in `_PRESET_KEY_PROVIDERS`, so `other_key_providers()` no longer excluded it. Gemini then leaked into the openai-family "Other provider" catch-all — whose tail is documented as "all openai-family" — and, sorting before `xai`, became picker entry #1. Selecting "Other → #1" stored the entry under the `gemini` family (KeyError: 'openai' in the add-other test). Gemini already has its own "Gemini — API key" top-level entry (gemini-family scoped), so it belongs in `_PRESET_KEY_PROVIDERS` like openai/anthropic/ openrouter. Add it there; update test_add_menu_options_ordering for the new first-party Gemini key entry and assert the gemini-family scoped subset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ap-web): stub AntigravityIcon in test-setup so suites load under vitest `SubagentsPanel.tsx` now imports `AntigravityIcon` (@lobehub/icons/es/ Antigravity), whose glyph drags in @lobehub/fluent-emoji → @emoji-mart/data. Those JSON modules need an import attribute that Node refuses under vitest, so every suite reaching SubagentsPanel (AddAgentDialog, AppShell.subagent-nav, SubagentsPanel) failed to LOAD — "needs an import attribute of type json". The sibling @lobehub icons (Claude/Codex/Cursor) are already stubbed here for the same broken-nested-resolution reason; AntigravityIcon was simply missing. Add the matching stub. Verified: with it the 3 suites load (negative control: without it SubagentsPanel.test.tsx fails to load on the fluent-emoji chain). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(antigravity-native): de-flake restart-cursor forwarder test `test_restart_with_persisted_cursor_emits_only_new_steps` waited for the emitted item event, then cancelled the forwarder and asserted the persisted cursor was 4. But the forwarder posts the item THEN advances the cursor, so the immediate cancel could interrupt before the cursor write landed — a CI-load race that failed as `assert 2 == 4`. Wait for the cursor itself (strictly stronger: it implies the item was already mirrored), mirroring the first-run loop. Stable across 20 local repeats; full forwarder file green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(onboarding): family-filter the "Other provider" tail at the chokepoint Adversarial review (codex) flagged that keeping gemini out of the openai-family "Other provider" picker via _PRESET_KEY_PROVIDERS alone is exclusion-list based: a future non-openai catalog family omitted from that tuple would leak into the openai-only catch-all again (the gemini bug, reincarnated). The "Other provider" option is openai-family scoped (_add_option_families), so converge the fix at the chokepoint — other_key_providers() now filters to OPENAI_FAMILY, not just the preset list. Zero behavior change today (the whole current tail is openai-family); it hardens the class of bug. Also note in the agy-stub fixture that the real missing-binary path is covered in test_antigravity_native_launch.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): address #892 review — durable SET resume cursor + tests Responds to PattaraS's 5 findings on PR #892: 1. Forwarder no longer drops a not-yet-written out-of-order step across a restart. The durable resume cursor is now the EXACT SET of acked step indices (forwarded_steps), suppressed by MEMBERSHIP, not a single <= high-water: agy writes step_index both non-contiguously AND out of order, so a <= floor advanced past a {12,14} batch silently dropped a later 13. The set is carried across same-conversation resume rewrites (_launch_and_record + runner auto-create) and materializes a legacy <=-floor into the set on upgrade. (bridge + forwarder + runner) 2. Pin the agy install: the bootstrapper has no version flag (always fetches latest from its auto-updater manifest), so the Dockerfile now fails the build when the installed agy != AGY_EXPECTED_VERSION (1.0.10) — a silent harness break becomes a conscious, visible bump. 3. Test the eager terminal-close finally seam (reattached / DETACHED). 4. Test the suppress-by-id branch (_dispatched_call_ids) directly — both arms. 5. Fix stale docstring: web turns inject via tmux send-keys, not connect-RPC SendAgentMessage (which agy logs as a SYSTEM_MESSAGE). Verified: 201 affected tests pass; ruff + format clean; a live omnigent end-to-end run confirms the out-of-order step survives a forwarder restart and renders in the web UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): CLI reattaches to runner-owned terminal (no double-launch) A fresh/cold-resume `omnigent antigravity` launch bound the runner and then ALSO ran `_launch_and_record`, double-launching the agy terminal: binding the runner triggers the runner's idempotent auto-create of `antigravity:main` (runner/app.py `_auto_create_antigravity_terminal`, which owns the terminal for every antigravity-native session), so the CLI's redundant terminal POST 500'd ("already observed as required") AND its `clear_bridge_state` wiped the bridge state the runner wrote — leaving the session `failed` and every web turn erroring with "Antigravity native bridge state is missing". Fix: after binding the runner, reattach to the runner-owned terminal (`_await_runner_antigravity_terminal` polls for it post-bind, mirroring the existing pre-bind resume reattach which can't catch the post-bind auto-create). A CLI-side launch stays only as a defensive fallback, so the change can only help or be neutral. Also corrects the now-stale "the runner has no agy auto-create branch" docstrings (the branch was added in 3666dbb0). Restores claude/codex parity for fresh CLI launches. Adds a regression test (fresh launch reattaches, never calls `_launch_and_record`) and keeps the cold-resume fallback test fast via a shortened wait. Verified: 168 affected tests pass; ruff + format + mypy clean. Live confirmation of a working send still pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): CLI defers forwarding to the runner on reattach Coupled follow-on to the double-launch fix, found in live testing: when the CLI reattaches to a runner-owned terminal it was STILL starting its own `supervise_forwarder` in `_attach_terminal`, while the runner already runs one (it auto-creates "terminal + forwarder" together). Two tailers POSTing the same agy transcript double-mirrored every step — verified live as duplicated chat messages and a duplicate one-time degrade notice. Fix: only start the CLI-side forwarder when NOT `prepared.reattached` (the fallback where the CLI launched its own terminal and is the sole mirror source); otherwise defer to the runner's forwarder. Same "runner owns the antigravity session" cleanup as the launch fix. Adds regression tests (reattached → no CLI forwarder; not-reattached → CLI forwards), counting the call deterministically rather than the cancellable task body. Verified live: with this + the launch fix, a fresh `omnigent antigravity` session sends from the web chat with no "bridge state missing", agy responds, and the reply mirrors back exactly once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): reattach on the local-server launch path (no double-launch/forward) The double-launch/double-forward fixes (7df3ba4d, f4ce3ce8) only patched the daemon prepare path (_prepare_antigravity_terminal_via_daemon). The default `omnigent antigravity` (local server) goes through _prepare_antigravity_terminal, which bound the runner then unconditionally called _launch_and_record with NO post-bind reattach -- racing the runner's _auto_create_antigravity_terminal exactly as the daemon path did. The local CLI usually wins (so it mostly worked), but when the runner wins, _launch_and_record's clear_bridge_state wipes the runner's bridge state (web turns fail "Antigravity native bridge state is missing"), its redundant terminal POST 500s, and reattached=False starts a second supervise_forwarder -> double-mirror. Mirror the daemon fix: after _bind_session_runner, poll for the runner-owned terminal (_await_runner_antigravity_terminal) and reattach (reattached=True) instead of launching; the CLI launch stays a defensive fallback. When no runner is bound (pure-local CLI), the path is unchanged (the CLI is the sole owner). Adds a regression test for the local path (fresh launch reattaches, never calls _launch_and_record). Found by adversarial review (gemini). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(antigravity-native): make the port-unresolved RPC test hermetic test_conversation_id_owned_by_pid_none_when_port_unresolved stubbed discover_language_server_port -> None but not _candidate_agy_rpc_ports, so when the pid-scoped port is unresolved the production fallback scanned EVERY live agy connect-RPC port. On any host/CI runner with a concurrent agy that fallback found real ports and ran _conversation_matches -> calls != [] -> the test failed (reproduced live by two reviewers). Stub _candidate_agy_rpc_ports -> [] too so the test exercises the genuine "no port from either source" branch hermetically. Source is unchanged (it correctly returns None either way). Found by review (gemini + opus). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(antigravity-native): correct RPC probe request/response shape; note sub-step at-least-once - antigravity_native_rpc.py module header described the GetConversationMetadata probe REQUEST as {"metadata": {"rootConversationId": ...}}, but the code sends {"conversationId": ...} and metadata.rootConversationId is the RESPONSE echo. Correct the header (request flat, response nested). - _post_events: note the at-least-once duplicate is also sub-step -- a step bundles a message + N function_calls, so one item's failed POST re-posts the whole step (re-emitting already-committed siblings) on restart. Found by review (gemini + opus). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(antigravity-native): RPC core rework design spec Design for reworking the antigravity-native harness runtime onto agy's connect-RPC surface (live-verified): structured trajectory-step reads (GetCascadeTrajectorySteps / StreamAgentStateUpdates) replacing JSONL transcript-tailing, interaction bridging (ask_question + run_command permission via HandleCascadeUserInteraction → omnigent elicitations), and a real interrupt (CancelCascadeSteps). Eliminates the transcript-mirror fragility class (out-of-order cursor, live double-render, user-message duplication) and closes the interactive-prompt gap. Periphery from #892 (onboarding/auth, registration, terminal infra, Docker pin, ap-web picker) is reused; turn-send stays on tmux send-keys pending a user-turn RPC. Wire shapes captured in memory agy-rpc-interaction-bridge.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(antigravity-native): RPC core rework implementation plan 13-task TDD plan for the RPC core rework (per the design spec): a discovery spike (turn-send + read-mode + step-type fixtures), the RPC client (trajectory steps / handle_user_interaction / cancel), a pure step→item mapper (no delta, skips USER_INPUT), the read driver, the interaction bridge with the timeout re-read loop, the server elicitation adapter + hook, real interrupt via CancelCascadeSteps, runner wiring, forwarder cutover, and live parity verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * spike(antigravity-native): record RPC step fixtures + turn-send/read-mode decisions Capture live agy 1.0.10 GetCascadeTrajectorySteps fixtures (11 live, 1 synthesized) covering every step type Tasks 4/5 map: USER_INPUT, PLANNER_RESPONSE (text + tool_call ask_question/run_command), RUN_COMMAND WAITING/DONE, ASK_QUESTION WAITING/DONE, plus CONVERSATION_HISTORY/CHECKPOINT/LIST_DIRECTORY; ERROR synthesized from the live WAITING shape (labelled, with _fixtureProvenance). Record decisions with evidence in docs/claude/antigravity-rpc-spike-notes.md: - turn-send: KEEP tmux send-keys (send-keys turn records as USER_INPUT with source USER_EXPLICIT; no user-turn RPC exists; SendAgentMessage mis-records as SYSTEM_MESSAGE). - read-mode: default StreamAgentStateUpdates (first steps frame ~130ms after a turn) with GetCascadeTrajectorySteps poll fallback; request MUST be connect-enveloped (bare JSON => protocol error). Poll-first is an acceptable de-scope. Also live-confirmed: permission + askQuestion answer round-trips (HandleCascadeUserInteraction => 200, step flips DONE); CancelCascadeSteps {cascadeId} => 200 but no-op on a WAITING-for-interaction step (Task 10 must validate cancel against RUNNING steps). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): RPC client — trajectory steps + cancel Add two unary connect-RPC methods mirroring _conversation_matches: - get_trajectory_steps(port, cascade_id) -> list[dict]: POSTs {"cascadeId": ...} to GetCascadeTrajectorySteps, returns resp["steps"]. - cancel_cascade_steps(port, cascade_id) -> bool: POSTs {"cascadeId": ...} to CancelCascadeSteps, returns True on HTTP < 400, False on error. Both respect _assert_loopback_url + _sync_client(_HTTP_TRANSPORT) so the MockTransport seam covers them in tests. Also adds the two method name constants alongside the existing _METHOD_FORCE_STOP_CASCADE_TREE. TDD: 2 new tests written first (RED: AttributeError), then impl (GREEN). Full file: 47/47 passing, ruff+mypy --strict clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): address Task 2 review — drop type:ignore, raise_for_status, fail-open test - Remove # type: ignore[arg-type] from test_get_trajectory_steps: narrow seen["body"] with isinstance(body, (bytes, bytearray)) before json.loads, so mypy accepts it without any suppression. - Add response.raise_for_status() in get_trajectory_steps before .json(): non-2xx responses (e.g. HTTP 500 "trajectory not found") may not be JSON, so decoding them would raise JSONDecodeError (undocumented). raise_for_status raises httpx.HTTPStatusError (subclass of httpx.HTTPError) on non-2xx, matching the documented :raises: and catchable at one site by Task 6. Updated docstring to explain the intentional raise (not fail-open) contract. - Add test_cancel_cascade_steps_false_on_transport_error: asserts the primary safety contract (ConnectError → False) that was previously untested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): RPC client — handle_user_interaction Add AntigravityRpcError exception class and handle_user_interaction() unary connect-RPC method to the existing antigravity_native_rpc module. Delivers interaction answers (question responses / approvals) to agy by POSTing to HandleCascadeUserInteraction with trajectoryId+stepIndex nested inside interaction (required by proto-JSON encoding). Raises AntigravityRpcError carrying the raw response body on non-2xx so Task 8 can detect the overloaded "input not registered for step N" race string. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): pure step→item mapper (no delta, skip USER_INPUT) Create omnigent/antigravity_native_steps.py with map_step_to_events() for the RPC-based read path. Fixes two live bugs: drops output_text_delta so the web UI no longer double-renders assistant text, and skips USER_INPUT steps so the user message is not duplicated (already persisted by direct POST /events). Handles CORTEX_STEP_TYPE_* format (camelCase fields, argumentsJson strings) rather than the transcript format. WAITING tool steps emit no output event; DONE steps emit function_call_output keyed via the FIFO allocator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): WAITING-interaction extractor Add PendingInteraction TypedDict and pending_interaction() to antigravity_native_steps. Returns None for DONE steps even when requestedInteraction is present (status-keyed, not field-keyed). Extracts trajectory_id via a new _trajectory_id() helper that mirrors _step_index(). 19 new fixture-driven tests; 55 total green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): surface is_multi_select in pending_interaction spec Add _merge_is_multi_select() helper that reads is_multi_select from metadata.toolCall.argumentsJson and injects it into a fresh copy of the requestedInteraction.askQuestion spec dict per question index. Defaults to False when argumentsJson is absent or malformed; never mutates the input step. 5 new tests (fixture False, synthetic True, absent json, malformed json, no-mutation); 60 total green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): address Codex review of RPC client — wrap transport errors, guard steps body, add tests CDX-IMP2: Wrap handle_user_interaction's client.post in try/except httpx.HTTPError; re-raise as AntigravityRpcError("transport error contacting agy: {e}") so the Task 8 bridge has one exception type for all delivery failures (transport and non-2xx alike). Non-2xx still raises AntigravityRpcError(response.text) to preserve the body for "input not registered" detection. Add test_handle_user_interaction_raises_rpc_error_on_transport_error. CDX-MIN4: Guard get_trajectory_steps response body against {"steps": null} or non-dict body: use isinstance checks before list() so a malformed 2xx can't raise TypeError. Document that non-JSON 200 raises ValueError (Task 6 driver catches broadly). CDX-MIN5: Add test_get_trajectory_steps_raises_on_500 — pins the non-2xx raises contract (not fail-open, unlike cancel). CDX-MIN6: Broaden cancel_cascade_steps except from httpx.HTTPError to Exception with comment explaining deliberate fail-open intent; covers ssl.SSLError and other errors outside the httpx hierarchy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): address Opus/Codex review of step mapper — real tool-call ids, slot-0 index, robustness OPUS-IMP1: use agy's real tool-call ids for function_call/output pairing. plannerResponse.toolCalls[].id on invocation and metadata.toolCall.id on result steps are used directly; _ToolCallIdAllocator is fallback-only when the id field is absent (resume-mid-turn). Out-of-order multi-result regression test verifies FIFO would mis-pair but real-id pairing is correct. CDX-IMP1 + OPUS-MIN1: _step_index accepts string-encoded ints (agy sends some numerics as strings) and treats a missing stepIndex as 0 (proto omits zero-valued scalars) rather than silently dropping the step. OPUS-MIN2 / Task4-M1: modifiedResponse precedence over response is now tested with a synthetic step where the two fields differ; the choice is documented (post-moderation text, present and equal to response in live fixtures). OPUS-MIN3 / Task4-M2: collapse dead double USER_INPUT guard into a single `if step_type == _TYPE_USER_INPUT: return []`. Task4-M3: remove unused _TYPE_CHECKPOINT / _TYPE_CONVERSATION_HISTORY constants (catch-all return [] handles them; keeping them added noise). CDX-MIN3: fix _SOURCE_USER comment ("model-generated" → "user-submitted input"). T5FIX-MIN: collapse redundant `except (json.JSONDecodeError, Exception)` in _merge_is_multi_select to `except Exception`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): drop test type:ignore, remove orphaned constant (review follow-up) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(antigravity-native): simplify RPC client + step mapper (code-simplifier pass) Move _METHOD_HANDLE_CASCADE_USER_INTERACTION to the top-level _METHOD_* constant block where all sibling method constants live, removing the out-of-place inline definition between AntigravityRpcError and handle_user_interaction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): RPC read driver Add omnigent/antigravity_native_reader.py: the read-path driver that replaces the transcript-tail forwarder's read loop. It discovers agy's cascade id (from bridge state, past the agy_conv_* placeholder) and connect-RPC port (port-first, conversation-ownership confirmed), then polls GetCascadeTrajectorySteps, maps each new step to Omnigent conversation items (Task 4 mapper), posts them, emits RUNNING/IDLE external_session_status edges on turn transitions (replicating TranscriptParser's stateful heuristic), and hands WAITING steps to the Task 8 interaction bridge via an on_pending_interaction callback. - Dedup by (trajectory_id, step_index) identity in an in-memory seen-set (no durable cursor — retired in Task 12); re-reads post nothing. - One _ToolCallIdAllocator per run; real agy ids keep pairing order-independent. - httpx.HTTPError (transport + non-2xx) and ValueError (non-JSON 200) on a poll are logged and swallowed; the loop never dies on a transient. - Injectable stop predicate bounds the loop under test. TDD: 9 tests (dedup, USER_INPUT-skip, WAITING-once, status transitions, error recovery, placeholder-wait). ruff + mypy --strict clean; no type:ignore / noqa. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(server): antigravity elicitation adapter Add pure shape-mapping adapter that converts a PendingInteraction dict (ask_question or permission) into ElicitationRequestParams for the web UI, and converts the ElicitationResult back into the HandleCascadeUserInteraction payload. Mirrors _codex_elicitation.py's ask_question/permission patterns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): interaction bridge with timeout re-read Add omnigent/antigravity_native_interactions.py: the detect→elicit→deliver bridge for the agy RPC harness. It surfaces a WAITING interaction as an Omnigent elicitation, awaits the verdict, and delivers it via HandleCascadeUserInteraction — handling agy's WAITING-interaction timeout gotcha (design §2.1): - re-reads the freshest WAITING step at delivery time (never the captured detection-time ids — agy may have timed the step out and retried at a higher stepIndex while the human deliberated); - on the overloaded HTTP 500 "input not registered for step N", re-reads for a NEW higher-index WAITING step and re-surfaces a fresh elicitation against it (new deterministic id per step_index); - bounds the loop with max_retries so a timeout-retry storm terminates; - returns (no delivery) on a None verdict (human timeout/cancel) and on any non-"input not registered" RPC error. Three async seams (get_steps / request_elicitation / deliver) keep the timeout logic unit-testable without a live agy. deliver defaults to a _deliver_via_rpc wrapper that offloads the sync handle_user_interaction to a worker thread (mirrors the Task 6 read driver), since the bridge is async. TDD: 9 unit tests (happy path, input-not-registered re-read, permission accept, staleness-before-first-delivery, None verdict, no-WAITING-step, non-retryable error, bounded retry storm, deterministic id). ruff + mypy --strict clean; no type: ignore / noqa. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(server): antigravity elicitation hook endpoint Add POST /v1/sessions/{session_id}/hooks/antigravity-elicitation-request — the runner→server bridge for the agy native interaction bridge (Task 8). The bridge POSTs {elicitation_id, params} here; the endpoint parks on the shared harness elicitation registry, emits response.elicitation_request for the web UI, awaits the approval verdict, then returns the raw ElicitationResult JSON (simpler than the codex hook: no JSON-RPC envelope to build — the bridge does that via to_interaction_payload). Timeout returns empty 200 so the bridge reads None and leaves the agy WAITING step to expire on its own. Mirrors the codex-elicitation-request path exactly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(antigravity-native): Phase 2 full-RPC-parity spec (turn-send, streaming, usage, model, rotation) All shapes live-verified against agy 1.0.10. Resolves the §7 turn-send open question (SendUserCascadeMessage) and adds streaming-delta / token-usage / model-change / new-conversation-rotation parity with the codex+claude harnesses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): RPC client — send_user_cascade_message + model catalog Adds two typed connect-RPC wrappers to antigravity_native_rpc.py (Task T-A): - send_user_cascade_message(port, cascade_id, text, *, plan_model) POSTs the exact verified body shape {cascadeId, items:[{text}], cascadeConfig:{plannerConfig:{planModel}}} to SendUserCascadeMessage, recording USER_INPUT (not SYSTEM_MESSAGE). Raises AntigravityRpcError on transport errors or HTTP >= 400, carrying the raw body so the executor can surface model/validation errors (e.g. "neither PlanModel nor RequestedModel specified"). Mirrors handle_user_interaction. - get_available_models(port) POSTs {} to GetAvailableModels and returns the parsed catalog {models:{<key>:{model, displayName, recommended, ...}}} for runtime model enum resolution. raise_for_status() on non-2xx; returns {} on a non-dict 200 body. Mirrors get_trajectory_steps error contract. TDD: 6 new tests (MockTransport, no live agy); all 58 tests pass. Ruff/mypy --strict clean; no # type: ignore or # noqa anywhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): RPC client — stream_agent_state_updates (connect server-stream) Add the connect-protocol server-stream client for agy's StreamAgentStateUpdates, the live-delta source the T-D streaming reader will consume. Opens a persistent streaming POST, reassembles connect frames from the raw byte stream, and yields each DATA frame's parsed JSON update dict in arrival order, stopping on the end-of-stream trailer. Framing (live-verified, agy 1.0.10; design §10.2): - Request: one connect-enveloped message [0x00][BE-len][{"conversationId"}], Content-Type application/connect+json (via new _encode_connect_envelope). - Response frames [flag][BE-len][payload]: flag 0x00 = data (yielded), flag & 0x02 = trailer (stop), flag & 0x01 = compressed (raise — agy sends uncompressed, so a set bit is a decode mismatch). - Buffer-based reassembly: one chunk is never assumed to be one frame — several frames may pack into a chunk and a frame (incl. its 5-byte header) may straddle chunks; a bytearray holds bytes until a full frame is present. Uses a dedicated _STREAM_TIMEOUT (read=None) so the long-poll is not aborted mid-turn; reuses _assert_loopback_url and the _async_client seam (signature widened to httpx.Timeout | float; docstring refreshed — it now has a live caller). TDD: 7 tests via httpx.MockTransport streaming responses (custom AsyncByteStream with controlled chunk boundaries) cover the request envelope, in-order multi-frame yields, split+packed frame reassembly, header-split reassembly, trailer termination, the compressed-frame raise, and the non-loopback URL refusal. mypy --strict clean; no type/lint suppressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): raise on connect trailer error in stream_agent_state_updates In connect server-streaming a mid-stream server failure is reported in the end-of-stream TRAILER PAYLOAD as {"error": {...}} — NOT via HTTP status, because the 200 + headers were already flushed before the failure. The previous code treated any flag & 0x02 trailer as a clean stop, making an errored stream indistinguishable from clean completion and silently truncating the turn for the T-D streaming consumer. stream_agent_state_updates now parses the trailer payload (new _connect_trailer_error helper, which fails safe toward a clean stop on an empty / non-JSON / non-object / no-error payload) and raises AntigravityRpcError carrying the stringified error when the trailer holds a non-empty error object. Clean trailers (empty payload, {}, or any payload without a truthy error) still return normally — behavior is otherwise identical. The framing layer is the right place for this so T-D gets one failure surface and does not have to inspect trailers itself. Tests (same MockTransport streaming style): an error trailer after data frames yields those frames then raises (asserting the data was delivered in order before the raise); empty-payload and {} trailers are clean stops. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): reader streaming mode (output_text_delta + poll fallback) Stream-primary read driver: consume StreamAgentStateUpdates for live output_text_delta typing parity, falling back to the committed-only poll loop on any stream error (httpx.HTTPError / AntigravityRpcError trailer). - Per GENERATING PLANNER_RESPONSE frame, prefix-diff plannerResponse.modifiedResponse and emit the new suffix as one external_output_text_delta (stable per-step message_id antigravity:<conv>:<step>:planner, final=False); commit the DONE message via the mapper afterward. Delta-first ordering + stable id satisfies the SPA single-render reconciliation contract. - Dedup committed items by (trajectory_id, step_index), recorded only once a step is SETTLED (DONE/ERROR/USER_INPUT) so a tool-result seen RUNNING before DONE is not deduped early and its output dropped (stream observes every status frame). - Relocate the delta builder out of the soon-retired forwarder into the mapper module as output_text_delta_event + planner_message_id (suffix + configurable final); the reader depends on the mapper, not the forwarder. - Reasoning-stream skipped: no external reasoning-delta POST contract exists; folding thinking into output_text_delta would corrupt the message (see report). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): gate committed planner message on DONE (no poll-path double-render) The mapper emitted a planner `message` at ANY status (only tool-results were DONE-gated). The poll fallback does not intercept GENERATING (only the stream path does), so a poll catching a planner GENERATING then DONE posted TWO messages for one step — the exact double-render the RPC rework removes, on the fallback path. Gate the PLANNER_RESPONSE committed items (message + function_calls) on status == DONE, symmetric with the existing tool-result gate. A non-DONE (GENERATING) planner now maps to [] — its partial text is conveyed only via the streaming reader's output_text_delta events. Effect: exactly one committed message with the FINAL text on BOTH the stream and poll paths; the stream still emits live deltas, the poll stays committed-only. The _is_settled tool-result dedup fix from the prior commit is retained and now consistent: a planner records `seen` only at DONE (when it produces committed items). All planner fixtures are DONE, so no Task-4 mapper test needed updating. Tests: poll-path regression (generating→done → one message, final text, no deltas); stream-path analog strengthened to assert final committed text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): reader telemetry — session usage + model change Implements design §10.3 (external_session_usage) and §10.4 (external_model_change) in the RPC read driver. - _model_usage_from_step: extracts agy string-int modelUsage fields (inputTokens/outputTokens/cacheReadTokens) from PLANNER_RESPONSE DONE steps; maps to cumulative_input_tokens/cumulative_output_tokens/ cumulative_cache_read_input_tokens + model (displayName). - _requested_model_enum_from_step: reads userInput.userConfig.plannerConfig.requestedModel.model from USER_INPUT. - _resolve_display_name: resolves enum→displayName via GetAvailableModels catalog; falls back to raw enum when unknown. - _ensure_catalog: fetches and caches the model catalog once per reader run (asyncio.to_thread); logs + returns {} on failure (best-effort). - _maybe_emit_session_usage / _maybe_emit_model_change: fired inside the key-not-in-seen branch of _process_committed_step so replay of already-seen steps never re-emits. Model-change deduped by state.posted_model_enum (raw enum, not displayName). - _ReaderState extended with posted_model_enum, model_catalog, port. - 7 new tests cover: usage emission + field mapping, usage replay dedup, missing-usage graceful skip, first-turn model-change, same-model no-re-emit, model switch mid-session, model replay dedup, unknown enum fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(antigravity-native): emit running cumulative session usage (SET-semantics) The server prices per-turn cost as delta = (new cumulative) - (old cumulative). Emitting agy's per-model-call inputTokens/outputTokens directly caused the server to compute a zero delta on turn 2+ (since each turn's per-call value was the same), freezing the cost badge after turn 1. Fix: accumulate per-call modelUsage values in _ReaderState and emit the running totals, matching codex's tokenUsage.total (cumulative, SET semantics). Also: - Thread the real step_index through to OutboundEvent for both usage and model-change events (was hardcoded to 0). - Add _ReaderState.cumulative_* reset comment for T-G /clear rotation. - Add test_two_turn_usage_is_cumulative regression guard: two turns of 1000 input tokens → turn 1 posts 1000, turn 2 posts 2000 (not 1000 again). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): RPC-driven executor — real interrupt + RPC turn-send Make AntigravityNativeExecutor fully RPC-driven, retiring the tmux send-keys write path (Task 10 + Task T-B): - interrupt_session: resolve cascade id (= conversation id) from bridge state, discover the connect-RPC port, and call CancelCascadeSteps. Documents the live-verified limitation (C3): cancel stops a RUNNING cascade and is a NO-OP on a WAITING-for-interaction step (a DENY via the interaction bridge unblocks that). Returns False on placeholder / no port / cancel failure. - run_turn + _deliver: deliver turns via SendUserCascadeMessage instead of send-keys. Per-turn planModel is resolved at runtime (two-tier, design §10.4): echo the latest USER_INPUT step's requestedModel.model, else fall back to the recommended GetAvailableModels entry. ExecutorConfig.model/effort stay informational (agy owns model selection on this write path). - First turn (Option A, pure RPC): on the agy_conv_* placeholder, wait for the runner to mint the real id (Task 11), then send; surface a clear "not ready" ExecutorError if it never lands rather than typing into the TUI to mint it. - AntigravityRpcError from the turn-send is surfaced (carrying agy's message), not swallowed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): RPC conversation cold-start bootstrap (StartCascade) The runner now mints the agy conversation over connect-RPC on a fresh host-spawned launch (StartCascade) instead of seeding only an agy_conv_* placeholder, so the executor's turn-1 has a real cascade_id. The existing supervise_forwarder spawn is kept (Task 11b swaps it for the reader) and now binds the cold-started conversation directly. - antigravity_native_rpc.start_cascade(port, cascade_id, *, source): POSTs {cascadeId, source} to StartCascade; 200 -> None, non-2xx/transport -> AntigravityRpcError (mirrors send_user_cascade_message). - runner.app._cold_start_agy_conversation: polls the Heartbeat-OK connect-RPC port (bounded), StartCascades a runner-minted uuid4, and overwrites bridge state's conversation_id with the real id via update_conversation_id. Best-effort/non-raising so a failure leaves the placeholder for the forwarder and never aborts the launch. Wired into _auto_create_antigravity_terminal on fresh (not resume) launches, after the terminal starts and before the forwarder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): runner wires RPC streaming reader + interaction bridge Swap the antigravity auto-create's transcript-forwarder spawn for the RPC streaming reader (supervise_reader, T-D) and wire its on_pending_interaction to the Task 8 interaction bridge via the Task 9 elicitation hook, making the full RPC chain live (cold-start 11a -> reader T-D -> bridge Task 8 -> hook Task 9 -> executor Task 10/T-B). 11a's cold-start is untouched; the reader replaces the forwarder only and reuses the same single-instance per-session task registry. - Widen OnPendingInteraction to (cascade_id, port, pending) so the bridge gets the SAME ids the reader discovered (no re-discovery race); thread them through the single delivery point in _process_committed_step. - Add production elicitation glue in app.py (_post_agy_elicitation_request, _request_agy_elicitation) mirroring codex's long-poll re-POST + body handling, and _run_antigravity_reader which owns the client and runs supervise_reader with the bridge-wired callback. - Tests: reader callbacks updated to the new contract (poll + stream paths assert cascade_id/port threading); auto-create harness stubs the reader; new end-to-end wiring test (pending -> hook POST {elicitation_id, params} -> handle_user_interaction delivery; task named antigravity-reader-{session_id}). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(antigravity-native): retire transcript forwarder + durable cursor (RPC reader supersedes) The RPC streaming reader (Task 11) replaced the transcript-tail forwarder on the runner path; this completes the full cutover (Option A) by migrating the last forwarder consumer — the CLI ``omnigent antigravity`` attach fallback — to the reader + interaction bridge, then deleting the forwarder and its now-dead durable read cursor. - Extract a shared ``run_reader_with_bridge`` helper into ``antigravity_native_reader`` (Omnigent client + elicitation POST/retry + ``on_pending``→``bridge_interaction`` + ``supervise_reader`` spawn). The runner's ``_run_antigravity_reader`` and the CLI ``_attach_terminal`` both call it; the elicitation machinery moves out of ``runner/app.py``. - CLI ``_attach_terminal`` (non-reattached fallback only) now spawns the reader + a one-shot cold-start as background tasks at attach-start (cancelled in ``finally``), mirroring the runner. agy is started on attach (``tmux_start_on_attach=True``), so cold-start + reader run concurrently with the attach and poll agy in; the post-hoc ``audit_policies`` path is dropped in favor of real-time elicitation. The fallback TUI shows the empty ``>`` banner because the cold-started RPC conversation is headless (documented). - Both cold-starts (CLI + runner) now PATCH the cold-started cascade id onto the session as ``external_session_id`` (best-effort, mirroring codex/pi) so a later ``--resume`` continues agy's actual conversation — the read-path replacement for the forwarder's ``_patch_external_session_id``. The CLI cold-start is guarded to run only on a placeholder id (skipped on resume), so ``--resume`` is not clobbered by a fresh ``StartCascade``. - Drop the durable read cursor (``forwarded_steps`` / ``forwarded_step_index`` / ``update_forwarded_*``) from bridge state and both launch paths; the reader uses an in-memory seen-set. Legacy on-disk cursor keys are tolerated and ignored. - Delete ``antigravity_native_forwarder`` + its test; sweep forwarder-era docstrings across the rpc/launch/reader/runner/CLI/audit/post-delivery modules. Behavior-preserving for the surviving paths (runner reader + CLI reattach); the existing suites passing is the proof. The relocated shared types (``OutboundEvent`` / ``_ToolCallIdAllocator`` / ``_AGENT_NAME`` / ``_TOOL_ARG_DISPLAY_KEYS``, now canonical in ``antigravity_native_steps``) are included here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): harden external_session_id cold-start PATCH against silent rejection (CLI+runner) Follow-up to the decision-2=(b) external_session_id PATCH (landed in the preceding commit): the best-effort PATCH only caught a transport ``httpx.HTTPError`` and ignored 4xx/5xx *responses* (httpx does not raise on those), so a server-side rejection — and the lost ``--resume`` continuity it implies — was silently swallowed on BOTH the CLI fallback and runner paths. - Inspect ``status_code`` after the PATCH and log a warning on ``>= 400`` on both ``_cold_start_agy_conversation`` (CLI) and ``_patch_agy_external_session_id`` (runner), mirroring the codex recorder PATCH. Still strictly best-effort: a rejection (or transport error) never raises, and the cascade id is already in bridge state so the chat mirror is unaffected; only resume fidelity degrades. - Add focused coverage for the runner best-effort helper (None-client no-op, transport-error swallow, 4xx-rejection warning) and a CLI 4xx-rejection test. - Fix a stale "resets the resume cursor" comment on the runner cold-start (the durable cursor was removed in the cutover) and remove a pre-existing ``type: ignore[arg-type]`` in the CLI test's ``_mock_client`` by typing the handler as ``Callable[[httpx.Request], httpx.Response]``. The placeholder/resume guard that makes ``--resume`` continue agy's prior conversation (skip cold-start + PATCH on a non-placeholder id) is intact on both paths and covered by tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(antigravity-native): cover legacy durable-cursor key tolerance on bridge read Addresses the Task 12 review's minor finding: the cutover removed the forwarded_step_index / forwarded_steps durable-cursor fields, and read_bridge_state must tolerate (ignore) them in a forwarder-era state.json. Extends the legacy-fields test to carry both cursor keys and asserts they are absent from the parsed dataclass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(antigravity-native): code-simplifier pass (readability, behavior-preserving) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): address 3-way review — functional-RPC timeout, IDLE-on-DONE gate, stream re-entry backoff, runner cold-start guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): run interaction bridge off the reader loop with single-in-flight guard 3-way review (codex+gemini, with a repro) found the reader loop blocked for the full duration of a human interaction: _maybe_handle_interaction awaited the elicitation long-poll (up to ~24h) inline, freezing streaming/tool-output/status and risking stream severance. The naive create_task fix the reviewers proposed would double-fire on agy's WAITING-timeout retry steps (it re-issues at a higher step_index), so this adds a single-in-flight guard: the bridge runs off-loop as a tracked _ReaderState.interaction_task; while one is active the loop skips spawning another (the in-flight bridge owns the retries via its own freshest-WAITING re-read); a done-callback clears the slot; supervise_reader cancels it on teardown. Tests: streaming continues while an interaction is pending (gemini's repro), single-in-flight guard suppresses a retry-step double-fire, done-callback clears the slot for a later interaction, and reader teardown cancels the in-flight task. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): scope cold-start to the session's agy pid (avoid wrong-agy cross-bind) The cold-start picked candidates[0] (the lowest Heartbeat-answering agy connect-RPC port). On a host running several agy instances under one runner (sub-agent fan-out, shared runner, `omnigent run --server` multi-session) this could StartCascade onto a FOREIGN agy and permanently bind the session to the wrong conversation, since no conversation exists yet to disambiguate. Scope the cold-start port to THIS session's own agy via its tmux pane: pane -> pane pid -> agy pid in the pane's process subtree -> that pid's connect-RPC port. agy is the pane process on the simple `exec agy` launch and a descendant (sandbox launcher -> bwrap -> agy) on a sandboxed launch, so the resolver checks the pane pid itself then walks descendants intersected with the live agy pids. Falls back to the existing candidate scan when no local pane is reachable (remote runner) or the pane cannot be resolved, so single-agy hosts and remote runners are unaffected; the fallback is logged. Both cold-starts (runner + CLI) are threaded the pane and share the new resolve_cold_start_agy_rpc_port helper. Placeholder/resume guards, the port-bind timeout/poll loop, and the external_session_id PATCH are preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): surface agy reasoning/thinking stream (parity) Gemini Thinking-model variants stream chain-of-thought at plannerResponse.thinking (design 10.2), which the RPC reader and step mapper never read — so reasoning was dropped, a parity gap vs the in-process antigravity executor (which emits the same reasoning SSE pair). Reader: mirror the modifiedResponse text-delta path for thinking — a new per-step reasoning prefix tracker on _ReaderState, _partial_planner_thinking extractor, and _emit_partial_reasoning_delta (prefix-diff suffix per GENERATING frame, started=True only on a step's first delta). Reasoning is emitted BEFORE the response delta (10.2 ordering) and the tracker is cleared on commit alongside the text tracker. A planner with no thinking emits nothing (no regression to text streaming). Steps mapper: output_reasoning_delta_event builder for the transient external_output_reasoning_delta event. Reasoning is delta-only — the mapper commits NO reasoning item (matching codex/claude/the in-process executor, none of which commit reasoning content); the SPA finalizes the reasoning block when the assistant message arrives. Server: external_output_reasoning_delta external event type publishes response.reasoning.started (once, when data.started) + response.reasoning_text.delta SSE — the events the SPA already maps (sse.ts) and renders (blockStream.ts). The reasoning-content wire bridge did not exist for native harnesses; only text (external_output_text_delta) and effort (external_reasoning_effort_change) did. Nothing is persisted. Tests: reader streaming (incremental reasoning deltas with started-once, reasoning-before-text ordering, no-thinking no-regression, no-growth dedup); mapper builder shape + no committed reasoning item on DONE-with-thinking; server route (started publishes both SSE, continuation publishes delta only, malformed delta rejected). No suppressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): cold-start keeps polling when the session's agy isn't up yet (no foreign-agy fallback) R2 review found a residual cross-bind on the CLI path. CLI terminals use `tmux_start_on_attach=True`, so the pane runs `tmux wait-for; exec agy` and agy is only exec'd when the human attaches — but the cold-start polls CONCURRENTLY with the attach. During that early-poll window the pane is just the shell, so the pane resolver found no agy and returned None, and `resolve_cold_start_agy_rpc_port` fell through to `_candidate_agy_rpc_ports()[0]`. If a foreign agy was the only candidate, StartCascade bound this session into the FOREIGN agy — the exact durable cross-bind the scoping targets. Fix: distinguish THREE pane states via a new `PaneAgyResolution` (`resolve_pane_agy_rpc_port_state`): 1. agy found + port resolved -> scoped port. 2. agy found + port unattributable -> candidate fallback (restricted /proc; one-agy-per-pod, so the lone candidate is ours — preserves k8s behavior). 3. NO agy found yet -> return None, keep polling (do NOT touch candidates — a foreign agy could be the only one). No pane supplied (remote runner) still falls back to candidates. Also: only thread the pane into the CLI cold-start when the tmux socket exists LOCALLY (mirror `_can_attach_direct_tmux`), so a remote runner's server-side socket path doesn't trigger ~80 doomed `tmux display-message` spawns per poll and correctly routes to the no-pane -> candidate path. `resolve_pane_agy_rpc_port` is retained as a thin port-only wrapper. Bounded deadline/poll loop, placeholder/resume guard, and external_session_id PATCH unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): guard multi-question askQuestion + detect stale /clear-rotated conversation Three R4 edge-guard fixes from the 3-way review. Fix A — multi-question askQuestion no longer broadcasts one answer to all. agy's askQuestion can carry several questions[i] (each with its own option ids + is_multi_select), and the agy wire wants one response entry PER question. But ElicitationResult.content is flat (one selectedOptionIds / writeInResponse, no per-question key), so the SPA can only collect a single answer end-to-end. The prior code broadcast that single answer to EVERY question — semantically wrong. Now we answer ONLY the first question and leave the rest to agy, logging the limitation. Single-question (the dominant, working case) is unchanged. Full per-question support needs a schema + SPA-form change and is flagged as a follow-up. Fix B — detect a TUI /clear that rotates the bound conversation. On the CLI-fallback path, a human running /clear in the agy TUI mints a NEW cascade id; the reader bound the old one at discovery and would keep mirroring the now-dead conversation silently. Each stream frame names the active conversation (update.conversationId, design §10.5); the reader now compares it to the bound cascade id and, on a mismatch, logs a clear warning and stops mirroring rather than failing silently. Absent/empty/ matching conversationId is not a rotation (false-positive-free on the normal path). Full automatic re-bind + Omnigent session rotation (T-G) is flagged as a follow-up; for the headless runner path it is obviated by the 1:1 design. Fix C — docstring nit (doc-only). output_reasoning_delta_event no longer claims it "matches the in-process executor (same SSE pair)"; the in-process antigravity executor emits only reasoning_text deltas and relies on an IMPLICIT reasoning-start, whereas this path emits an EXPLICIT response.reasoning.started. Both end with no committed reasoning item. Tests: multi-question answers only the first + does not broadcast + logs (single-question stays silent); a rotated conversationId stops+warns and does not mirror the dead step, while matching/absent ids do not false-fire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(antigravity-native): hedge /clear-rotation guard field path as unverified (R4 review) R4 review found Fix B's premise — that StreamAgentStateUpdates frames carry ``conversationId`` at the frame top level (design §10.5) — is UNVERIFIED and contradicted by the evidence: real stream captures show steps frames only as ``update.mainTrajectoryUpdate.stepsUpdate.steps[]``, and the only live-verified conversation-id echo is NESTED (``metadata.rootConversationId`` from GetConversationMetadata). §10.5 is planning intent (rotation tagged unimplemented follow-up T-G), and the reader test is self-referential (hand-sets the field). The control flow is correct (the early ``return`` is terminal — it does NOT fall through to the guard-less poll loop), and the field-path FIX needs a live capture that can only be taken during Task 13 (live-e2e). So this commit makes the code honest rather than guessing: docstrings/comments now flag the top-level field path as a design ASSUMPTION pending a Task 13 live ``/clear`` capture (dump the raw post-rotation frame; if the id is nested, fix ``_frame_conversation_id`` and swap the hand-built helper for a captured fixture). Also notes the two-axis uncertainty (field location + whether a foreign frame ever reaches this stream — §10.5 names GetAllCascadeTrajectories as the PRIMARY signal; this per-frame check is only the secondary one). Doc/comment-only; no behavior change. Fix A (multi-question guard) and Fix C (reasoning docstring) reviewed correct and unchanged. 43 reader tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(antigravity-native): code-simplifier pass (readability, behavior-preserving) Behavior-preserving readability cleanup over the antigravity-native RPC rework. No logic, signature, or control-flow changes; all gates green (ruff/mypy/pytest). - antigravity_native.py: R5 docstring consolidation. Folded the scattered historical references to retired mechanisms (transcript-tail forwarder, durable resume cursor, tmux send-keys) into one concise, accurate preamble at the top of the module docstring. Trimmed the now-redundant repetitions in the read/write bullet, the _launch_and_record docstring + inline comment, and the _attach_terminal note, while keeping the locally load-bearing facts (the dropped pre-tool audit / no refresh-capable reader auth, and the _patch_external_session_id "replacement for the retired forwarder's id capture" notes). - antigravity_native_rpc.py: extracted the byte-identical POST+raise tail shared by handle_user_interaction, send_user_cascade_message, and start_cascade into a private _post_rpc_raising(port, method, body) helper. Removes ~33 lines of duplication; each caller now just builds its body and delegates. Identical wire behavior (URL, headers, JSON body, transport-error wrapping, raw-body raise on >=400). - antigravity_native_steps.py: extracted the repeated metadata.sourceTrajectoryStepInfo navigation shared by _step_index and _trajectory_id into a private _source_traj_info(step) accessor. - antigravity_native_reader.py, antigravity_native_interactions.py, inner/antigravity_native_executor.py, server/routes/_antigravity_elicitation.py: unchanged — reviewed, no redundancy worth removing without behavior/clarity risk (and the reader's /clear-rotation honesty hedges are deliberately preserved). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): 3-way re-review fixes — USER_INPUT dedup, reasoning re-anchor, stream guards, observability I-1 (ship-blocker): antigravity_native_steps.py + antigravity_native_reader.py — USER_INPUT dedup-key collision. USER_INPUT steps have a per-conversation-stable trajectory_id and no stepIndex, so every turn's USER_INPUT collided on (trajectory_id, None) and was silently de-duped after turn 1 (no per-turn RUNNING/IDLE status edge, no model-change). Added _execution_discriminator (executionId/createdAt) and widened _StepKey to a 3-tuple, folding the discriminator in only for steps that lack a stepIndex. Steps WITH a stepIndex key as (traj, idx, None) — unchanged dedup for seen/interacted (interaction and content steps always carry a stepIndex). Test now uses real per-turn executionId (no synthetic stepIndex): test_two_real_wire_turns_each_emit_running_then_idle + test_step_key_distinct_for_user_input_turns_without_step_index + TestExecutionDiscriminator. A (important): antigravity_native_reader.py — _emit_partial_reasoning_delta re-anchored reasoning_prefixes[idx] only inside the growth branch, so a non-monotonic thinking rewrite froze reasoning deltas permanently. Moved the re-anchor out of the if (mirrors the text path). Test: test_stream_reasoning_reanchors_after_non_monotonic_rewrite. B (important): antigravity_native_rpc.py — stream_agent_state_updates wrapped the DATA-frame json.loads; a malformed frame raised a bare JSONDecodeError that the supervisor does not catch (reader died silently, no poll-fallback). Now raises AntigravityRpcError. Test: test_stream_agent_state_updates_raises_on_malformed_json_frame. C (important): antigravity_native_bridge.py — update_conversation_id now returns bool and logs a WARNING (naming the dropped id) on a None state read instead of silently dropping the real cascade id. Both cold-start callers (antigravity_native.py, runner/app.py) check the result and warn on False. Test: test_update_conversation_id_returns_false_and_warns_when_no_state. D (minor): antigravity_native_rpc.py — stream_agent_state_updates now checks response.status_code >= 400 right after the stream opens (httpx stream() does not raise on non-2xx; an unframed error body looked like a clean empty stream and reconnected forever). Used the explicit status_code form to avoid httpx streaming-body read issues. Routes into the reader's poll-fallback. Test: test_stream_agent_state_updates_raises_on_non_2xx_status. E (minor): antigravity_native_interactions.py — _freshest_waiting dropped the cross-kind any_kind fallback; it now returns strictly same-kind (or None), since agy keys delivery on trajectoryId+stepIndex with no kind check. Tests: test_freshest_waiting_returns_none_for_only_different_kind + test_freshest_waiting_returns_highest_same_kind. F (minor): antigravity_native_interactions.py + antigravity_native_reader.py — reworded the bridge's no-verdict log so it no longer claims timeout/cancel exclusively (hook rejection also yields None); enriched the reader's elicitation 4xx WARNING to flag a likely misconfigured hook. Log wording only. G (minor): antigravity_native_interactions.py — the "input not registered" race discriminator is now matched case-insensitively (str(exc).lower()), so a capitalization change in agy's 500 body cannot reclassify the retryable race as fatal and drop the human's verdict. Test: test_input_not_registered_match_is_case_insensitive. Gates: ruff clean; mypy unchanged at 29 pre-existing baseline errors (0 new); 587 tests pass across the antigravity-native suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): correct GetAvailableModels/USER_INPUT-model/stream-frame wire envelopes (live e2e) + real-wire fixtures A live e2e against agy 1.0.10 proved the branch's three RPC wire envelopes were wrong; the prior synthetic fixtures encoded the wrong shapes, so the tests passed while the real wire failed every turn. Captured the real wire and corrected both the code and the fixtures. BUG 1 (FATAL — model resolution failed every turn): GetAvailableModels returns {"response": {"models": ...}}, not {"models": ...} at the top level. get_available_models now unwraps body["response"] (falling back to the body itself defensively, {} for a non-dict), so both consumers (_recommended_model, _resolve_display_name) read catalog["models"] again. The get_available_models test now mocks {"response": {...}} and asserts the unwrapped catalog; consumer tests already used the post-unwrap shape. BUG 2 (FATAL — tier-1 model echo always None): the live USER_INPUT step carries plannerConfig.planModel as a STRING (the same field send_user_cascade_message sends), not requestedModel.model (a dict). Executor _latest_requested_model and reader _requested_model_enum_from_step now read planModel first and fall back to requestedModel.model for any TUI-origin step using the old shape. Fixtures relocated requestedModel -> planModel (steps/user_input.json; reader helpers _user_input_with_model / _user_input_real_wire; executor helper _steps_with_model); model-change and echo tests keep the same expected enums. Added one focused fallback test on each side (reader + executor) to keep the requestedModel.model path covered. BUG 3 (CRITICAL — stream mirrored nothing): each StreamAgentStateUpdates DATA frame is a connect envelope {"update": {...}}; the reader read mainTrajectoryUpdate/conversationId at the top level, so every frame yielded 0 steps and the stream-primary reader mirrored nothing (a 0-step frame does not raise, so poll-fallback never fired). The generator now unwraps parsed["update"] (falling back to the parsed dict defensively) before yielding, so the reader's _frame_steps/_frame_conversation_id work unchanged. The rpc-stream tests now build {"update": {...}} frames (via _data_frame) and assert the generator yields the unwrapped payload; a new test covers the no-envelope defensive fallback. Reader tests feed logical (post-unwrap) frames and are unchanged. All three fixes verified against the captured agy 1.0.10 wire. The Fix B /clear rotation guard is intentionally untouched (a separate follow-up replaces it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(antigravity-native): real /clear rotation via GetAllCascadeTrajectories (T-G), replacing the dead per-frame guard The R4 per-frame /clear guard was a proven no-op: a StreamAgentStateUpdates stream is bound to ONE cascade and only ever reports THAT cascade's id, so a per-frame "did the conversation change?" check can never observe a sibling conversation. This replaces it with real, out-of-band rotation detection + automatic Omnigent session rotation, mirroring the codex forwarder. STEP 1 (RPC primitive). antigravity_native_rpc.get_all_cascade_trajectories: POSTs {} to GetAllCascadeTrajectories, raise_for_status (NOT fail-open, like get_trajectory_steps/get_available_models), returns the parsed body (the trajectorySummaries map). Documented with the live-verified shape. STEP 2 (pure detection). antigravity_native_reader._detect_rotated_cascade: selects the newest-active ROOT cascade (trajectoryType CORTEX_TRAJECTORY_TYPE_- CASCADE) by lastUserInputTime (falling back to lastModifiedTime), parsing ISO- 8601 robustly (trailing Z -> UTC). Rotates only when the current cascade differs from the bound one AND is strictly newer than the bound entry's own activity; returns None when the bound entry is absent (never rotate blindly), when the newer entry is a bare /clear mint (no activity timestamps yet), or for a non-CASCADE (subagent) sibling. STEP 3 (session rotation). _rotate_session_for_cascade mirrors codex's _create_thread_replacement_session API sequence: GET old snapshot -> POST /v1/sessions (old agent_id + INHERITED labels, so the new session resolves to the SAME bridge_dir; agy's bridge_dir is keyed off the launcher bridge-id, not the session id) -> PATCH runner_id -> PATCH external_session_id=new cascade -> POST terminal /transfer -> write_bridge_state(new session+cascade) -> PATCH old runner_id="". Best-effort: any failure logs a WARNING and returns None (the reader keeps the old binding). Bridge state is rewritten only after the new session is created+bound, so a mid-sequence failure never points it at a half-created session. STEP 4 (wire-up). supervise_reader spawns a _watch_for_rotation background task that polls GetAllCascadeTrajectories every few seconds (the stream cannot see a sibling); on detection it flips the body's stop and supervise_reader returns the new cascade id. run_reader_with_bridge now LOOPS: bind -> supervise -> on a returned cascade id, _rotate_session_for_cascade -> rebind (re-enter supervise, which rediscovers from the rewritten bridge state with a fresh _ReaderState). A failed rotation keeps the old binding and adds the cascade to skip_cascade_ids so it never hot-loops detect->fail->detect. The elicitation hook reads the current session id through a holder so a post-rotation interaction targets the new session. Existing teardown (interaction-task cancel in finally) is preserved and now also cancels the rotation detector. STEP 5 (cleanup). Removed the dead per-frame guard (_frame_names_other_- conversation, _frame_conversation_id, the rotation check + R4 honesty-hedge comments in _stream_loop) and the reader test helper _frame_with_conversation + the two /clear-rotation reader tests it backed. Updated stale comments/docstrings that referenced the dead guard or the unverified top-level conversationId field path (superseded by T-G). Tests: get_all_cascade_trajectories (returns/non-dict/500); _detect_rotated_- cascade (newer sibling, minted-unused, only-bound, older, non-cascade, bound- absent, lastModifiedTime fallback, equal-activity, malformed ts, real capture); supervise_reader returns the new cascade on rotation + honours skip_cascade_ids; _rotate_session_for_cascade exact codex API sequence + bridge-state write + None on create failure; run_reader_with_bridge rebind loop (advances session id) + keeps-old-binding-on-failure. mypy: 29 pre-existing, 0 new. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): actuate /clear rotation by cancelling the wedged stream (T-G deadlock) The Task T-G /clear-rotation reader DETECTED a rotation but never ACTUATED it. `supervise_reader` ran the rotation detector concurrently with the reader body, but `await`ed the body DIRECTLY (`_stream_loop`, falling back to `_poll_loop`). When the detector fired it set `rotation_holder` and flipped `_body_should_stop()` to True — but that stop is only re-checked at `_stream_loop`'s outer `while` and after its inner `async for`. After a TUI /clear the bound cascade goes IDLE and the connect stream blocks forever inside `aiter_bytes()` (the idle long-poll uses a deliberately deadline-less read), so neither checkpoint is reached: `_stream_loop` never returns, the `finally` never runs, `supervise_reader` never returns, and `run_reader_with_bridge` never calls `_rotate_session_for_cascade`. No replacement session, no terminal transfer, no rebind — web turns kept targeting the dead conversation. Found by a live e2e. Fix: run the reader body as a cancellable task (`antigravity-reader-body`) and have the rotation callback cancel it in addition to recording the new cascade id. Cancellation raises CancelledError inside `aiter_bytes()`, which unwinds `stream_agent_state_updates`' `async with` cleanly (httpx supports cancellation) where a cooperative stop re-check cannot run. The body task is created BEFORE the detector starts (referenced via a holder) so the callback can never fire before the task exists. `await body_task` distinguishes a ROTATION cancel (rotation_holder set → fall through and return the new id) from an EXTERNAL shutdown cancel (rotation_holder empty → re-raise so it propagates, never a phantom rotation). The existing finally still cancels the rotation + interaction tasks in the documented order, and now also finalizes the body task on every exit path so nothing leaks. Neither `_stream_loop` nor the generator catches CancelledError (their excepts cover only httpx.HTTPError / AntigravityRpcError), so the cancel is not swallowed. Adds a regression test that wedges the stream on a never-firing event (the live /clear-then-idle shape) with the detector reporting a rotation, and asserts `supervise_reader` RETURNS the new cascade id under a tight `wait_for` budget (a regression times out loudly instead of hanging the suite); plus a test that an external cancel of a wedged reader propagates CancelledError rather than being mistaken for a rotation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): suppress runner turn-lifecycle idle (live-e2e double-idle) Live e2e found every web turn emitted a premature response.completed (0 items) + session.status idle at ~0.3s, THEN the real reasoning/text/usage ~1.8s later against the already-completed response (spinner stops, then text appears). Root cause: the runner's `_publish_turn_status` (runner/app.py) suppresses the turn-lifecycle session.status edge for terminal-backed harnesses whose status is owned by a native observer — claude/pi/cursor-native suppress BOTH running+idle, codex-native suppresses idle (its injection task returns before the model turn). antigravity-native was in NEITHER set, so its turn-lifecycle running+idle leaked alongside the RPC reader's own edges. The executor's SendUserCascadeMessage returns the instant agy accepts the turn, so the runner's idle fires ~2s before agy streams output; the server derives response.completed from that idle, hence the empty premature completion. Fix: antigravity-native shares codex's shape — add it to the codex-native idle suppression (publish `running` for immediate accept feedback; the RPC read driver owns the accurate `idle` once agy's output completes). The server then keeps the response in_progress until the reader's real idle, so output streams into the live response instead of after a phantom completion. Tests: parametrized test_message_turn_lifecycle_status_suppressed_for_terminal_backed_harnesses now covers antigravity-native (expected ["running"], no idle). 610 antigravity-surface tests pass; mypy unchanged at the 29-error pre-existing baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): /clear rotation at claude parity (transfer existing agy, no external_session_id, no auto-cold-start loop) A live e2e proved the prior T-G /clear rotation infinite-loops, spawning ~1 orphan agy + session every 3-5s. Root cause: the rotation POSTed a new session AND PATCHed its external_session_id=new_cascade. But POST /v1/sessions for an antigravity-native session makes the runner auto-cold-start a brand-new agy (_auto_create_antigravity_terminal fired for EVERY such session), which minted its OWN cascade AND set the new session's external_session_id. The rotation's external_session_id PATCH then hit that already-set, set-once-immutable field -> 400 -> rotation aborted; but the cold-start had already rebound the reader to its fresh cascade -> the detector re-fired -> infinite session-spawn loop. This mirrors claude's _create_clear_replacement_session, which already does /clear rotation correctly. agy, like claude, is ONE long-lived process hosting many cascades; a /clear mints a new cascade on the SAME process, so the replacement TRANSFERS the existing terminal (it does NOT re-spawn) and rewrites bridge state so the reader rebinds to the new cascade on the same process. Two changes, both copied from claude: 1. _rotate_session_for_cascade (antigravity_native_reader.py): drop the external_session_id PATCH entirely (claude never does it — the new cascade is already live on the existing agy, reached via the rewritten bridge state, not via a later --resume). New sequence: GET old snapshot -> POST /v1/sessions (agent_id + inherited bridge-id label) -> PATCH runner_id -> terminal /transfer old->new -> write_bridge_state(session_id=new, conversation_id=Y) -> clear old runner_id. The bridge-state write lands AFTER the transfer, so the runner's auto-create guard (below) still sees the OLD session owning the terminal while the new session binds. 2. The auto-cold-start-avoidance mechanism, replicated exactly from claude: claude gates _auto_create_claude_terminal on _terminal_inbound, computed by _claude_native_terminal_arrives_via_transfer — it reads the shared bridge's active session and returns True when a DIFFERENT session on the same bridge owns a live terminal (the one about to transfer in), so auto-create skips. It's race-free because the rotation writes the new active-session marker only AFTER the transfer, so at bind time the bridge still names the old terminal-owning session. Added the antigravity mirror _antigravity_native_terminal_arrives_via_transfer (reads read_bridge_state().session_id against the antigravity:main terminal) and wired the antigravity branch with the same _antigravity_inbound gate + "rotation target" skip log. After a successful rotation the reader is bound to Y; GetAllCascadeTrajectories shows Y as the most-recently-active root cascade == bound, so _detect_rotated_cascade returns None and the detector does not re-fire. Tests: rewrote the rotation sequence test to assert the claude sequence and that NO external_session_id PATCH is made; added a parametrized runner guard test (mirroring the claude one) proving an antigravity rotation-target session does NOT trigger _auto_create_antigravity_terminal while fresh/dead-terminal sessions still do. Verified the guard is load-bearing (neutering it reds the rotation-target case). Found by live e2e. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(antigravity-native): record T-D poll-path double-render follow-up (2960b9b2) in SDD report Accurate SDD report update documenting the earlier poll-path double-render fix (commit 2960b9b2): map_step_to_events now DONE-gates PLANNER_RESPONSE committed items symmetrically with the tool-result gate, so both stream and poll paths post exactly one final message. Left unstaged across the session; committed now to finish with a clean working tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(antigravity-native): document /clear-before-first-turn rationale in _detect_rotated_cascade Behavior-identical comment clarification. The bound_activity-is-None branch (rotate to any active sibling) is INTENTIONAL: it handles the /clear-before-first-turn case (a freshly-bound cascade that never took a turn, then a sibling the user actually used) — staying bound there would strand the reader on the dead pre-/clear cascade. A final-review pass proposed "hardening" this to stay-bound; that would regress this reachable case, so the comment now records why the branch exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): close every tool call in the step mapper (P0 #2) The RPC step mapper emitted a `function_call` for every entry in `plannerResponse.toolCalls` unconditionally, but only emitted a paired `function_call_output` for three result types (RUN_COMMAND / LIST_DIRECTORY / ASK_QUESTION) at DONE with non-empty text. Three common paths therefore left a permanently-dangling `function_call` (the reader is the sole completion signal and the server pairs strictly by call_id, so an unpaired call renders a perpetual in-progress tool card): (a) result types with no extractor (VIEW_FILE / CODE_ACTION, live on agy 1.0.10) fell through to `return []`; (b) terminal-ERROR tool steps (e.g. an ignored/timed-out interactive prompt that flips WAITING->ERROR) returned []; (c) a successful RUN_COMMAND whose `combinedOutput.full` is proto3- omitted (cd / mkdir / redirects) returned []. Fix: treat a step as a tool result when it is a known type OR carries a `metadata.toolCall.id`, and on a terminal status (DONE/ERROR) always emit exactly one `function_call_output` keyed on that id — type-specific text when available, an error marker on ERROR, else an empty string. WAITING / RUNNING / PENDING still emit nothing (no result yet). System steps with no toolCall.id (CHECKPOINT / CONVERSATION_HISTORY) remain skipped. Tests: flip the ERROR test to assert a paired error output, add closure coverage for empty-output DONE commands and unmapped result types, and a guard that id-less system steps are still skipped. 84 mapper + 102 reader tests pass. Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com> Co-authored-by: Isaac * fix(antigravity-native): close the turn on a terminal/degenerate planner (P0 #4) The reader opened a turn (RUNNING) on USER_INPUT but only closed it (IDLE) on a DONE PLANNER_RESPONSE that carried assistant text and no tool calls. A turn that ended in any other terminal shape — a terminal-ERROR planner, or a DONE planner with neither text nor a tool call — never fired IDLE, so `turn_active` stuck True: the web/mobile spinner spun forever AND the next turn's USER_INPUT could not re-open RUNNING (it is gated on `not turn_active`), leaving the UI frozen. Add `_is_turn_close_step`, used by `_emit_step` in place of the narrower `_is_assistant_text_close_step`: a turn now also closes on a terminal-ERROR PLANNER_RESPONSE and on a DONE PLANNER_RESPONSE that dispatches no tool call (degenerate end). A planner that DOES dispatch a tool call is still a continuation (never a close), and non-planner/tool-result steps never close (a recovery planner follows). The existing text-close predicate and its tests are unchanged. Known follow-up (out of scope here): a turn interrupted mid-flight from the agy TUI where agy emits no terminal planner step still relies on the next planner to close; a periodic reconciliation against agy's cascade status would cover that fully. Tests: 5 predicate cases + an integration test proving an ERROR-planner turn emits RUNNING then IDLE. 69 reader tests pass. Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com> Co-authored-by: Isaac * fix(antigravity-native): make agy ask_question round-trip over the web UI (P0 #3) The agy elicitation adapter stamped the question under the params key `ask_question` and expected the web verdict to carry `selectedOptionIds`. But the SPA only renders the interactive AskUserQuestion form off the `ask_user_question` key, and that form posts a flat `{question -> selected label(s)}` map — it never produces `selectedOptionIds`. So an agy ask_question rendered as a generic approve/reject card and, on accept, the adapter received `content=None` and delivered `{"askQuestion": {"responses": []}}` — the user's actual choice was silently dropped. Fix (reuses the existing, tested SPA form — no behavioral frontend change): - `_agy_ask_question_params` now also stamps the question under `ask_user_question` in the Claude AskUserQuestion shape (agy option `text` -> Claude option `label`; each question gets a synthetic string id == its index). The raw agy spec stays under `ask_question` for the reverse mapping. - `_agy_ask_question_response` now consumes the form's answer map (keyed by question id, valued by selected labels / custom text) and maps each label back to its agy option id by matching option `text`; unmatched labels become `writeInResponse`. EVERY question is answered, so the prior single-question limitation is gone — multi-question prompts round-trip fully. - ApprovalCard: title agy prompts "Antigravity needs your input" instead of defaulting to "Claude has questions" (mirrors the codex branch). Tests: rewrote the adapter interaction-payload tests to the real form shape, added `ask_user_question` params coverage + multi-question round-trip, updated the bridge interaction tests, and added a frontend title test. Adapter/interactions (105) + ApprovalCard (35) pass. Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com> Co-authored-by: Isaac * fix(executor-adapter): drop id-less ToolCallComplete instead of emitting an empty-call_id output (P0 #1) The shared `ExecutorAdapter` replaced the old blanket suppression (`if self._current_ctx is not None: return`) with an id-scoped check (`call_id = ... or ""; if call_id and call_id in self._dispatched_call_ids: return`) so internal-tool executors (antigravity) could surface their own tool outputs. But the `or ""` coercion left the id-less path UNGUARDED: `if call_id and ...` is False for `call_id == ""`, so an id-less `ToolCallComplete` now fell through and emitted a `function_call_output` with `call_id == ""`. `ExecutorAdapter` is shared by every adapter-backed harness. pi emits its `ToolCallRequest`/`ToolCallComplete` with no metadata/call_id at all (omnigent/inner/pi_executor.py:2140,2211), so this fired deterministically: an empty-id output cannot pair (downstream pairs STRICTLY by call_id and discards empty ones) and rendered a stray ghost "Waiting for output" card — a regression vs main, whose blanket rule suppressed these. claude-sdk / cursor / openai-agents are reachable via the same id-less path. Fix: suppress BOTH a dispatched id AND an empty call_id (`if not call_id or call_id in self._dispatched_call_ids: return`). This restores main's suppression for id-less completions while keeping the PR's real-id emission for internal-tool executors (antigravity stamps a real positional id, so its completions still emit and pair). This matches the contract the code comments and the sibling test `test_internal_errored_tool_complete_emits_output_with_real_call_id` already assert ("must NOT carry call_id == ''"). Also fixes the `tool_call` mock harness, which modeled an unrealistic asymmetric shape (request with a real call_id, completion id-less) — a real handles_tools_internally executor stamps the id on both, so the mock now does too, and its observed function_call + function_call_output pair. Tests: add `test_idless_tool_complete_is_suppressed`; the adapter suite + antigravity(sdk/native) + claude-sdk + codex + cursor + copilot + openai-agents + pi executor suites all pass (590 tests). NOTE (for human review): this is shared code across 7 harnesses. Unit suites are green, but a live multi-harness smoke (pi + claude-sdk tool rendering) is worth doing before merge. Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com> Co-authored-by: Isaac * fix(ci): regen openapi.json, exclude antigravity-native from live matrix, reformat Three failures surfaced once the security gate was waived and the gated jobs ran for the first time: - Pytest `test_openapi_drift`: the committed `openapi.json` was stale. Regenerated via `scripts/dump_openapi.py` so it includes the new `/v1/sessions/{id}/hooks/antigravity-elicitation-request` endpoint (and the `external_output_reasoning_delta` post_event docstring pulled in by the main merge). - E2E `test_run_harness_live_matrix_covers_registered_coding_harnesses`: `antigravity-native` is a registered coding harness but a terminal-first TUI launched via `omnigent antigravity` (not `omnigent run --harness ...`) AND is Gemini-native (no Databricks-gateway probe wiring), so it is excluded from `expected_live_harnesses` like claude-native / goose-native / antigravity. - Pre-commit ruff-format: reformat `tests/test_antigravity_native_interactions.py` (the P0 #3 content-shape edit shortened those calls enough to fit on one line; ruff-format collapses them). Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com> Co-authored-by: Isaac * fix(antigravity-native): use the functional RPC timeout for model + cascade reads get_available_models and get_all_cascade_trajectories are FUNCTIONAL connect-RPCs but were built on the tight _PROBE_TIMEOUT_S (2s) reserved for port-discovery probes. The module's own timeout policy (antigravity_native_rpc.py:100-115) mandates _RPC_CALL_TIMEOUT_S (30s) for functional calls: a 2s deadline raises an un-retried TimeoutException against a momentarily-busy agy. - get_available_models resolves the per-turn model enum on the send path with no retry (executor._resolve_plan_model); a 2s abort surfaced a spurious "no model" error and failed the turn instead of completing it. - get_all_cascade_trajectories is the /clear-rotation functional poll (morally a step-read, like get_trajectory_steps which already uses 30s). Connection-refused (a force-killed agy port) still raises ConnectError immediately — not subject to the read timeout — so the wider deadline only adds headroom for an alive-but-busy agy; it never delays the dead-port path (verified live: ConnectError in <20ms against a refused port). Discovery probes (_heartbeat_ok, _conversation_matches) keep _PROBE_TIMEOUT_S. Tests updated to assert both functions now use the functional timeout and that the probes are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(antigravity-native): log the rotation detector's benign ConnectError at DEBUG _watch_for_rotation polls GetAllCascadeTrajectories every few seconds. When the agy port is gone — torn down / rotated / shut down before this fire-and-forget detector is cancelled — each tick raises httpx.ConnectError (connection refused) and was logged at WARNING, spamming the log during an otherwise-clean teardown. Add a ConnectError arm that logs at DEBUG and continues; the broad (httpx.HTTPError, ValueError) arm is unchanged, so a hung-but-listening port (ReadTimeout) and every other fault still WARN. Control flow is identical (both continue). A genuinely dead agy stays loudly visible: the reader BODY (stream + poll-fallback) independently WARNs on the path that matters; this only de-dups the secondary detector's redundant noise. Tests: a real-ConnectError tick logs exactly one DEBUG record and zero WARNINGs while the loop retries; a ReadTimeout tick still logs WARNING. Live-verified through the real _watch_for_rotation against a real OS connection-refused port (2 ConnectError ticks -> 2 DEBUG, 0 WARNING, no rotation, no leak). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(server): make the top-level-elicitations guard environment-invariant test_top_level_elicitations_route_is_not_mounted asserted a flat 404, but create_app mounts a catch-all SPA (Mount path="") whenever a local web-ui build exists at omnigent/server/static/web-ui/ (a gitignored dev artifact, absent on main/CI). Starlette's StaticFiles matches any path but rejects a non-GET method with 405, so the test passed on CI (404) yet failed in a worktree with a local SPA build (405) — environment-fragile, unrelated to whether the legacy route is mounted. Harden it to express the real contract two complementary ways: - route table (app fixture): no APIRoute serves POST /v1/elicitations/{id} (catches an exact re-mount even if its handler would 404 at runtime). - HTTP (client fixture, same app): status is 404 or 405 — both mean "no handler ran". A re-mounted legacy handler returns 400/501/2xx for this body, never 404/405, so the guard still bites. Passes with and without the local SPA build present. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ap-web): render native session for /compact composer tests (#1139 fallout) PR #1139 ("hide /compact for non-native harnesses") gated the /compact slash command behind `showCompact = isNativeWrapper`, but did not update ChatPage.composer.test.tsx — three tests there use /compact as the representative first built-in command (default highlight, ArrowDown target, and the effort-visibility anchor) and render via composerProps() whose default isNativeWrapper is false, so /compact is now hidden and the assertions fail (`Unable to find [data-testid="slash-menu-item-compact"]`). Render those three tests as a native-wrapper session (isNativeWrapper: true) so /compact appears, matching #1139's intent. The default helper is left non-native so the /model-routing test that relies on it is unchanged. Note: this breakage also exists on main (ChatPage.tsx + this test file are identical there); the same fix applies upstream. Co-authored-by: Bryan Li <bryanli@users.noreply.github.com> Co-authored-by: Isaac <isaac@example.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: SabhyaC26 <sabhyachhabria@gmail.com> Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com> Co-authored-by: Bryan Li <bryanli@users.noreply.github.com> Co-authored-by: Isaac <isaac@example.com> |
||
|
|
417b914a4d |
feat(qwen): add native-qwen TUI harness with resume, readiness gate, and clean-exit (#1134)
Add a terminal-native Qwen Code harness (`qwen-native`, alias `native-qwen`) that embeds the live `qwen` TUI in the web UI, alongside the existing ACP `qwen` harness. Unlike the goose/cursor tmux-send-keys natives, it drives qwen's built-in remote-control protocol: web turns are appended to qwen's `--input-file` and the transcript is mirrored back by tailing the structured `--json-file` event stream. Highlights (all verified against qwen v0.18.1-preview.1): - Bridge/executor/forwarder/CLI-wrapper + full registration (harness registry, aliases, native-coding-agent, wrapper labels, install spec, readiness, resume dispatch, resource role, server built-in seeding so Qwen Code shows in the new-session picker). - Readiness gate: the executor waits for qwen's first `system` event before the first submit, fixing the boot-order race where a message appended before qwen's input watcher started was silently dropped. - Session resume via the `external_session_id` convention (consistent with claude-/codex-/pi-native, fork-capable): deterministic per-conversation qwen session id, `--session-id` on first launch, `--resume` once a recording exists; qwen restores its own TUI history and emits only new events, so no double-mirroring. - Clean TUI quit: a qwen required-terminal exit is treated as a normal shutdown (publishes idle, no `required_terminal_exited` crash card). - Web UI: terminal pane recognized as an agent terminal; composer hides the model/effort chip for vendor-owned-model native sessions. Docs: docs/QWEN_NATIVE_DESIGN.md (design) and docs/QWEN_FOLLOWUPS.md (elicitation card, usage/cost/model surfacing tracked as follow-ups). Tests: executor, CLI wrapper, bridge/forwarder, server seeding, and web (nativeCodingAgents, chatStore flags, useTerminals, statusLine). Co-authored-by: Isaac |
||
|
|
bf2e1e9454 |
feat(harness): add OpenCode (native-server: serve + SSE forwarder + TUI takeover) (#576)
* docs(design): opencode harness + unified harness-interface (draft)
* docs(design): full opencode-native + unified harness-interface design
Covers: harness core (HTTP+SSE), opencode TUI attach takeover, ap-web
integration, opencode optional+runtime-selectable for polly & debby,
and the unified HarnessDescriptor/NativeServerHarness interface.
Supersedes the v1 draft.
* feat(opencode): harness core + unified native-server interface (fronts A, E)
Add the opencode-native harness and the HarnessDescriptor single-registration
that the scattered registries now derive from.
Front A (opencode core):
- opencode_native_bridge/state: per-session bridge dir, XDG roots, auth
secret, durable launch state.
- opencode_native_client: typed HTTP+SSE client shaped from the pinned
opencode 1.17.x OpenAPI (sessions/prompt/abort/fork/permission + /event).
- opencode_native_app_server: opencode serve process manager (loopback,
version-check, readiness) + attach argv/env builders.
- opencode_native_forwarder: SSE -> Omnigent event translation per the
design table (session.next.* text/tool/step, permission.v2.asked), dedupe,
reconnect.
- opencode_native_permissions: normalize + once/always/reject mapping.
- inner/opencode_native_executor + harness: thin create_app wrapper built on
the shared NativeServerHarness base.
Front E (unified interface):
- runtime/harness_descriptors: HarnessDescriptor + HARNESS_DESCRIPTORS, the
single source of truth; _HARNESS_MODULES / OMNIGENT_HARNESSES /
HARNESS_ALIASES / NATIVE_HARNESSES now derive from it.
- native_server_transport: NativeServerTransport protocol + dataclasses.
- native_server_harness: shared Executor base for native-server harnesses.
- opencode_http_transport + codex_ws_transport: two concrete transports
proving the abstraction.
Registries wired for opencode-native: spec allowlist, runtime modules,
aliases, native set, model-override (via native), install metadata,
readiness gating, wrapper label, native_coding_agents, built-in agent
seeding, and runner harness spawn-env.
Co-authored-by: Isaac
* feat(opencode): runner-owned serve + attach terminal takeover (front B)
Add the runner-side native terminal auto-create for opencode-native,
mirroring _auto_create_codex_terminal:
- _opencode_native_launch_config: fetch + validate the session snapshot.
- _auto_create_opencode_terminal: boot opencode serve, resume-or-create the
OpenCode session, persist external_session_id + bridge state, start the
SSE forwarder (supervised so the server is closed on teardown), and
register the `opencode attach` TUI as a streamable terminal resource.
- ensure_native_terminal dispatch branch for terminal_name == "opencode".
- OPENCODE_NATIVE_TERMINAL_ROLE constant.
The forwarder stays live independent of TUI process lifetime, so human
TUI actions keep mirroring into the web transcript.
Co-authored-by: Isaac
* feat(opencode): optional worker for polly/debby + allowlisted args.harness (front D)
Short-term (declared optional worker):
- examples/polly/agents/opencode and examples/debby/agents/opencode: optional
opencode-native workers, default-off (gated by `opencode` CLI presence).
- polly config: roster up to FOUR sub-agents, preflight probes `opencode`,
cross-review tracks harness AND model provider (opencode = 4th vendor, not
independent of same-provider implementers).
- debby config: optional third "OpenCode perspective", default fanout stays
Claude + GPT; three-way debate only on explicit request.
Long-term (runtime harness override):
- sys_session_send args gains an optional `harness` field.
- tool_dispatch validates it against the sub-agent's
executor.config.allowed_harnesses allowlist + OMNIGENT_HARNESSES and threads
it as harness_override into the child create (rejected on by-session-id mode).
- examples/polly/agents/codex opts in via allowed_harnesses:
[codex-native, opencode-native].
- conversation.harness_override docstring: a sub-agent may carry its OWN
create-time override (it still never inherits the parent brain's).
The server create route already validates + persists harness_override and the
runner already honors it, so the long-term path works end to end.
Co-authored-by: Isaac
* test(opencode): harness test matrix + conformance suite + scaffold generator (front E)
- tests/harness_conformance/: drift tests asserting every scattered registry
derives from HARNESS_DESCRIPTORS, plus the NativeServerTransport contract
driving NativeServerHarness over a fake transport AND both real transports
(OpenCodeHttpTransport via a fake HTTP server, CodexWsTransport via a fake
app-server client) — two implementations proving the abstraction.
- opencode unit tests mirroring the codex matrix: bridge state, launch state,
permissions mapping, HTTP/SSE client (httpx.MockTransport fake server, SSE
framing), app-server arg/env/version/start, forwarder translation table
(text/tool/step/permission/dedupe/filter/reconnect), executor turn lifecycle
(inject/abort/enqueue/image-block/mismatch).
- omnigent/scaffold_harness.py: dev generator for new-harness boilerplate +
the extension-point checklist.
104 new tests, all green.
Co-authored-by: Isaac
* feat(opencode): wire OpenCode into ap-web native UI (front C)
Mirror codex/pi native-agent wiring for OpenCode:
- OpenCodeIcon (@lobehub/icons/es/OpenCode); "opencode" added to the
NativeCodingAgentIconKind / ConversationIconKind unions.
- nativeCodingAgents.ts: OpenCode entry (opencode-native-ui / opencode-native,
sortRank 25, approvalMode) — derived lookup maps pick it up.
- NewChatDialog (display order + builtin set), SubagentsPanel (child icon +
subagent wrapper label), AgentCard (icon), sidebarNav (icon kind),
useTerminals (terminal_opencode_main excluded from the shell inventory).
- test-setup.ts: global OpenCodeIcon mock paralleling the Claude/Codex mocks
(the @lobehub icon import chain breaks under vitest otherwise).
- Tests extended across nativeCodingAgents / AgentCard / useAvailableAgents /
SubagentsPanel / sidebarNav / useTerminals.
tsc -b clean; vitest 2838 passed / 3 expected-fail / 2 skipped.
Co-authored-by: Isaac
* test(opencode): front D worker discovery + args.harness dispatch + readiness map
- test_opencode_polly_debby_worker: polly/debby specs declare the opencode
worker; codex worker allowlists the opencode-native override; preflight
probes opencode; debby keeps it optional.
- test_subagent_harness_override: args.harness extraction + allowlist
canonicalization helpers.
- harness_readiness test: opencode-native / native-opencode spellings added to
the configured-harness-map coverage assertion.
Co-authored-by: Isaac
* fix(opencode): eliminate mypy no-any-return at the transport/forwarder JSON boundary
Wrap the opaque JSON-RPC / SSE return values so the typed return contracts
hold (bool / str / Mapping), leaving only the explicit-any annotations the
repo sanctions for opaque JSON payloads (matching the existing codex modules).
Co-authored-by: Isaac
* test: update polly/debby worker-set expectations for the opencode worker
The optional opencode worker joins polly (4 workers, 4 vendors, 7 function
policies) and debby (3 workers, 3 vendors; default fanout still claude+gpt).
Update the brain-harness-override test and the example-bundle parse tests
accordingly.
Co-authored-by: Isaac
* fix(opencode): allowlist-gate args.harness schema + reconcile CI
Front D advertised args.harness unconditionally in the sys_session_send
schema, which broke two tests pinning the base args object to
{input, purpose, model} and diverged from design D.4 (the runtime harness
override is allowlist-gated, opt-in only).
- spawn.py: advertise `harness` in the args object only when at least one
declared sub-agent opts in via executor.config.allowed_harnesses (mirrors
the per-child dispatch guard in tool_dispatch.py). Specs without the
opt-in keep the base {input, purpose, model} contract, so the two pinned
schema tests stay correct as-is.
- test_sys_session.py: add a test asserting `harness` is present for an
opted-in sub-agent and absent otherwise (and that a mix opts the tool in).
- test_run_harness_without_agent_e2e.py: exclude opencode-native from the
live `omnigent run --harness` matrix. It is a terminal-takeover
native-server harness (same shape as claude/codex-native), so it cannot
round-trip through this gateway-backed no-AGENT matrix. Fixes E2E shard 1/4.
- test_start_session.py: add a hermetic e2e_ui Playwright test covering the
OpenCode agent in the new-chat picker (harness-derived "OpenCode" label,
not the raw "opencode-native-ui") and the terminal-first wrapper labels on
create.
Co-authored-by: Isaac
* fix(opencode): wire permission policy gate + per-prompt model pin
Addresses blocking cross-vendor review findings on the OpenCode harness.
BLOCKING #1 — security: OpenCode permissions no longer silently auto-approve.
- opencode_native_forwarder.py: the permission ``default_decision`` flips
from ``allow_once`` to ``reject``. An unconfigured or unreachable policy
now FAILS CLOSED — a headless OpenCode turn can never silently approve a
sensitive op. Only an explicit policy ``allow`` reaches ``once``/``always``.
- runner/app.py: wire a real ``policy_evaluator`` at forwarder
instantiation. ``_build_opencode_policy_evaluator`` POSTs each
``permission.v2.asked`` to the session's ``/v1/sessions/{id}/policies/evaluate``
endpoint as a ``PHASE_TOOL_CALL`` event — the SAME server-side gate
codex-native's policy hook uses, where an ``ask`` verdict is parked as a
human approval card and blocks until resolved. Unreachable / non-200 /
malformed / unresolved-ask all fail closed to deny.
- tests: assert no auto-approve absent policy, explicit allow → once,
allow_always → always, deny/ask → reject, the evaluator receives the
normalized policy input, and the runner evaluator's request shape +
verdict mapping + fail-closed paths.
BLOCKING #2 — OpenCode model override now governs the run from turn one.
- Verified against the OpenCode SDK that ``POST /session`` does NOT accept a
model (the stale client docstring is corrected); the model is a per-prompt
field ``{providerID, modelID}``. OpenCodeNativeExecutor now threads the
session's ``model_override`` (from bridge state) onto every injected
prompt. OpenCode persists the last-used model as the session default, so
pinning the first turn also governs later TUI-typed turns — the override
controls the run from the start, not only a later web turn.
- test asserts the resolved model reaches the prompt body as
``{"providerID","modelID"}`` (and is absent when no override is set).
NON-BLOCKING — tighten OpenCode server env isolation.
- opencode_native_app_server.py: drop ``OPENCODE_CONFIG`` /
``OPENCODE_CONFIG_CONTENT`` from the env passthrough so the parent shell's
GLOBAL OpenCode config can't defeat the per-session XDG isolation. Other
``OPENCODE_*`` vars (and the server password we set) are unaffected.
BLOCKING #3 (NativeServerHarness migration of codex-native) is NOT included:
a behavior-preserving migration is not safely landable here — see the PR
discussion. codex-native is unchanged; its executor tests stay green.
Co-authored-by: Isaac
* fix(opencode): address AI-review static-analysis nits + add deferral note
Resolve all 11 github-code-quality[bot]/CodeQL findings on PR #576,
all low-severity static-analysis nits with no behavior change:
- opencode_native_executor.py: rename subclass methods so they no longer
shadow the base NativeServerHarness instance attributes set from the
injected callbacks (_build_prompt -> _build_prompt_with_model_override,
_resolve_session_id -> _resolve_opencode_session_id). Bodies unchanged.
- native_server_transport.py: replace every `...` Protocol-method body
with `raise NotImplementedError` so CodeQL's "statement has no effect"
doesn't re-flag the stragglers. Interface semantics unchanged.
- opencode_native_bridge.py: document the two intentionally-ignored read
errors in ensure_auth_secret (missing/unreadable secret => regenerate).
Also append a "Deferred to a follow-up PR" section to the design doc
documenting that codex-native is not yet migrated onto NativeServerHarness
and CodexWsTransport is defined but not wired into any production path.
* fix(opencode): address CodeQL static-analysis nits
- test_opencode_native_forwarder: import the forwarder module one way only
(consolidate to `import ... as fwd_mod`, drop the duplicate import-from),
clearing CodeQL "module imported with import and import-from".
- codex_ws_transport / opencode_http_transport: export the client-factory
type aliases (`CodexClientFactory`, `ClientFactory`) via `__all__`. They are
the documented annotation for each transport's `client_factory` param, but
PEP 563 stringifies that use so CodeQL saw them as unused globals.
Co-authored-by: Isaac
* test(e2e-ui): regenerate visual baselines
* docs: drop opencode design doc from the PR (kept locally)
The 2k-line design doc inflated the PR diff without being code under
review. Untracked from the PR tree; it stays on disk locally for reference.
Co-authored-by: Isaac
* feat(opencode): web-UI terminal auto-create + Databricks-gateway provider wiring
Two gaps surfaced by a full-stack host e2e (isolated $HOME, real opencode serve):
1. Web-UI terminal auto-create: opencode-native was MISSING from the runner's
session-creation terminal dispatch (claude/codex/pi/cursor each have a
branch; opencode only had the on-demand ensure_native_terminal path). A
host/web-UI opencode session therefore never booted its opencode serve + SSE
forwarder + opencode attach terminal, so the UI had no terminal+chat view to
embed. Add the opencode-native branch alongside the other natives (idempotent
with the on-demand path via the existing per-session lock).
2. Databricks-gateway provider config: unlike codex/claude/pi (which consume
HARNESS_*_GATEWAY_* env their CLI translates), opencode reads provider/auth
from its own config file. Add omnigent/opencode_native_provider.py to resolve
a gateway from the spec's Databricks profile (via databricks-sdk) and
synthesize an opencode.json (custom @ai-sdk/openai-compatible provider at
{host}/serving-endpoints) into the per-session XDG config dir at spawn, with
the per-prompt model pinned to provider/endpoint. Best-effort: no profile or
no SDK -> opencode falls back to its ambient provider config.
Tests:
- tests/test_opencode_native_provider.py (13): synthesis shape, 0600 write,
model normalization, SDK-absent/no-token/success resolution.
- tests/e2e/test_host_opencode_native_e2e.py (opt-in OMNIGENT_E2E_OPENCODE_NATIVE):
built-in agent registered + host session auto-creates terminal_opencode_main.
Validated against the real Databricks AI gateway (databricks-claude-sonnet-4-6):
resolve -> synthesized opencode.json -> prompt round-trip returns assistant text.
Co-authored-by: Isaac
* fix(opencode): mirror assistant output to the web chat view + add `opencode` alias
#2 (chat view): the SSE forwarder was keyed on a `session.next.*` /
`permission.v2.asked` event vocabulary that opencode 1.17.x never emits, so every
real assistant-text/tool event hit `_HANDLERS.get(...) -> None` and was silently
dropped — the TUI showed the turn but nothing reached the web chat view (the
durable items the chat reads). The old unit tests passed only because they fed
the same fake event names.
Rewrite the handlers against opencode's real PART-based model (verified by
capturing a live `opencode serve` turn):
- text: `message.part.updated`(type=text, role-filtered to assistant) finalized
into a durable conversation item on `step-finish`/`session.idle`, plus
`message.part.delta`(field=text) streamed live (ephemeral);
- tools: `message.part.updated`(type=tool) — call posted once its `state.input`
is populated, output once `state.status` is completed/error (deduped by callID);
- lifecycle: `message.updated`(info.role), `session.status`(busy), `session.idle`;
- permissions: register both `permission.asked` (1.17.x) and `permission.v2.asked`.
Resume-dedupe is made type-aware so a reconnect never re-posts finalized parts.
Validated against a real Databricks-gateway turn: assistant text + bash tool
call/output now post as durable chat items; 17 forwarder unit tests rewritten to
the real event shapes (incl. user-text-not-mirrored + tool-snapshot dedup).
#3 (alias): accept `opencode` as a friendly alias for `opencode-native` (no
separate SDK `opencode` harness exists, so the bare name is free); added to the
descriptor `aliases` + `runtime_aliases`.
Co-authored-by: Isaac
* feat(opencode): show OpenCode in the `omni setup` harness picker
#1 (setup picker): OpenCode was absent from the `omni setup` harness overview, so
there was no obvious place to set it up. Add an OpenCode row (readiness = is the
`opencode` CLI installed) plus a `_manage_opencode_harness` drill-in that installs
the CLI when missing and explains where its credential actually lives — OpenCode
is a native-server harness with no Omnigent-stored key of its own; it routes
through the bound agent's Databricks gateway profile (synthesized into opencode's
per-session config) or ambient OpenAI-/Anthropic-compatible env vars.
Co-authored-by: Isaac
* feat(opencode): `omni opencode` CLI launcher + pin the setup install to 1.17.x
#4 (CLI launcher): `omni --harness opencode-native` errored "No native terminal
launcher wired" because opencode had no `run_*_native` launcher (every native
harness ships its own). Add one, mirroring `omnigent codex` / `omnigent pi`:
- `run_opencode_native` (omnigent/opencode_native.py): ensure a local daemon +
runner, create-or-resume the `opencode-native-ui` session (whose runner
auto-creates the `opencode serve` + `opencode attach` terminal — the branch
added that dispatch), then attach this TTY directly to the runner-owned tmux
pane. Reuses the shared `native_terminal` / `host.daemon_launch` helpers and
the same direct-tmux attach codex/pi use.
- An `omnigent opencode` command (resume/--model/passthrough args), and the
missing `native_agent.key == "opencode"` dispatch arm so
`omni run --harness opencode-native` routes here too.
Install version pin: `omni setup` → install OpenCode ran `npm install -g
opencode-ai`, but that package's npm `latest` is a broken `0.0.0-beta-*`
pre-release — so it installed a version the runtime version-check rejects. Pin
the install spec to `opencode-ai@~1.17.7` (mirrors the runtime
>=1.17.7,<1.18.0 range), so setup installs a working opencode.
Validated on an isolated-home daemon: the host-created opencode session
auto-creates `terminal_opencode_main` with the `tmux_socket`/`tmux_target`
metadata the launcher attaches to.
Co-authored-by: Isaac
* fix(opencode): stop emitting unreconciled live text deltas to the web chat
Follow-up to the forwarder rewrite. Posting `external_output_text_delta` for
opencode's `message.part.delta` left the web chat view broken: the UI builds a
`live:<message_id>` streaming-preview block from text deltas and only retires it
via a finalize/retire handshake (a `final=True` delta / authoritative done +
itemId reconciliation). The forwarder never completed that handshake and the
committed item carried no correlating id, so the live preview lingered alongside
the separate committed message — duplicated / garbled assistant text in chat
(the terminal/TUI was unaffected).
Drop the live-delta path: forward only the durable `external_conversation_item`
(role=assistant, full text), exactly the codex-native finalized-message path
that renders correctly today. The assistant message now appears cleanly when
each step completes. Removed the now-dead `_on_part_delta` / `_post_text_delta`
/ `next_text_index` / `_EXTERNAL_TEXT_DELTA`.
Live token-by-token streaming is deferred to a follow-up: it must match the web
UI's live-preview retire protocol (claude-native style) and be verified against
the real chat renderer, which can't be checked from a headless harness.
Reproduced via a real gateway turn: before, the forwarder posted a delta
(message_id `opencode:ses:text:prt`) AND a committed item (response_id `ses`)
with no correlation; after, only `running` → assistant item → `idle`.
Co-authored-by: Isaac
* fix(opencode): per-turn response_id so chat messages keep conversation order
Reported symptom: in the web chat, all assistant messages clustered together,
separated from the user messages, instead of interleaving per turn.
Cause: the forwarder stamped EVERY mirrored item with
``response_id = opencode_session_id`` — a single constant for the whole
session. The chat view groups items into a "response" by ``response_id``, so a
constant id collapsed every turn's assistant text/tool items into one response
block, which the renderer placed at the first item's position — pulling all
assistant output above the later user messages. (codex-native avoids this by
stamping a per-turn response id.)
Fix: stamp each item with opencode's per-assistant-message ``messageID`` as the
``response_id`` (falling back to the session id only when unknown), so each
turn is its own response group and items order by position as a normal
conversation. Threaded the messageID through `_post_assistant_text` /
`_post_tool_call` / `_post_tool_output` and the text/tool handlers.
Verified on a real 2-turn gateway conversation: the two assistant messages now
carry two DISTINCT response_ids (were one shared id before). Added a unit test
asserting per-turn response_ids + response_id assertions on the existing
text/tool tests.
Co-authored-by: Isaac
* fix(opencode): mirror user messages in the forwarder so chat keeps turn order
Reported: the web chat showed every assistant message clustered first, then the
user messages out of order (and one missing) — while the TUI was correct.
Root cause: for native-server harnesses the forwarder is the SOLE source of the
conversation transcript — omnigent does NOT separately persist a user item for
these sessions (the runner mirrors the native transcript; cf. runner/app.py's
`is_native_harness` history gate, and codex-native's `_post_user_message` /
`_ensure_user_message_posted`, which exist precisely because omnigent doesn't
record it). The opencode forwarder SKIPPED user-role text, so user messages were
never durably recorded; the chat only showed transient optimistic echoes —
inconsistent and unordered. (The earlier per-turn response_id fix was necessary
but not sufficient: the user items weren't being persisted at all.)
Fix: mirror the user message in the forwarder. On a user-role `message.part.updated`
text part, post a `role=user` conversation item EAGERLY (deduped by part id) so it
takes an earlier position than its assistant reply — matching codex-native. User +
assistant now interleave by turn. Resume dedupe pre-marks user-text parts too.
Unit-tested (forwarder now posts user-before-assistant, deduped, with a per-turn
response_id). The full multi-turn render is covered by the opt-in host e2e
(`test_opencode_native_multiturn_item_order`, asserts strict user/assistant
interleaving) for CI + manual QA.
Co-authored-by: Isaac
* chore(opencode): drop the 35k-line vendored OpenAPI dump from the PR
The vendored `omnigent/opencode/openapi-1.17.7.json` (34,576 lines) was ~80% of
the PR diff and made it unreviewable (goose's comparable harness PR is ~5k). It
was added to make the descriptor's `openapi_schema` reference real, but the
typed client is hand-maintained and the live wire-contract e2e
(`test_opencode_native_wire_contract_e2e`, opt-in) validates it against a real
`opencode serve` — a far better drift guard than a checked-in schema dump.
Remove the file and the descriptor's `openapi_schema` field (defaults to None).
The conformance check that vendored schemas exist still guards any future
descriptor that sets the field; it just skips when none do.
Co-authored-by: Isaac
* feat(opencode): make the `omni setup` OpenCode section manage providers
Before, the OpenCode setup drill-in just printed a static note — it did nothing
useful. Now it mirrors the Goose/Qwen pattern.
New read-only reporter `omnigent/onboarding/opencode_auth.py`
(`opencode_auth_summary`): reads OpenCode's own credential state — stored
providers from `~/.local/share/opencode/auth.json` (XDG_DATA_HOME-aware, JSON
keyed by provider id per the OpenCode source) + detected provider env keys
(OPENAI_API_KEY / ANTHROPIC_API_KEY / …). Robust: reads auth.json directly
rather than scraping `opencode auth list` output.
The drill-in now reports which providers OpenCode can reach and offers
`opencode auth login`, `opencode auth list`, and a help note — never storing a
key through Omnigent (OpenCode owns its auth; the Databricks-gateway path stays
the agent profile synthesized into opencode's per-session config). The setup
overview row's ✓/✗ now reflects real readiness (CLI installed AND a provider
reachable), not just the binary being present.
+ unit tests for the reporter (auth.json parsing, env detection, readiness).
Co-authored-by: Isaac
* refactor(opencode): ship the harness the scattered way; defer the unified interface
Splits PR #576 in two. This PR adds OpenCode as a harness exactly like
goose/qwen/cursor-native were added — scattered registration across the
hand-maintained registries — and DEFERS the unified-interface refactor
(the single-source ``HarnessDescriptor`` registry, the descriptor-parity
conformance suite, and the harness scaffold generator) to a follow-up so this
PR can be reviewed as a focused harness addition.
Removed (moves to the follow-up):
- omnigent/runtime/harness_descriptors.py — the HarnessDescriptor registry.
- omnigent/scaffold_harness.py — the new-harness scaffold generator.
- omnigent/codex_ws_transport.py — the (unused) codex WS transport that
generalized the native-server transport for a future codex migration.
- tests/harness_conformance/ — the descriptor-parity / transport-contract /
scaffold conformance suite.
Re-scattered the registration that Front E had made descriptor-derived, adding
OpenCode the old way alongside the existing harnesses:
- runtime/harnesses/__init__.py: ``_HARNESS_MODULES`` back to a literal dict
(+ ``opencode-native`` and its ``opencode`` runtime alias).
- harness_aliases.py: ``HARNESS_ALIASES`` / ``NATIVE_HARNESSES`` back to
literals (+ ``opencode`` / ``native-opencode`` → ``opencode-native``).
- spec/_omnigent_compat.py: ``OMNIGENT_HARNESSES`` / ``OMNIGENT_HARNESS_ALIASES``
back to literals (+ opencode id and aliases).
- onboarding/harness_install.py: ``_HARNESS_NAME_TO_KEY`` back to the
alias-keyed map (+ opencode), ``required_cli_for_harness`` back to the direct
lookup (no ``descriptor_for``).
Decoupled the kept OpenCode runtime from the descriptor registry:
- native_server_harness.py: take ``harness_id`` + ``supports_enqueue`` directly
instead of a ``HarnessDescriptor``.
- inner/opencode_native_executor.py: pass those literals.
- native_server_transport.py / opencode_http_transport.py: drop the
CodexWsTransport docstring references.
The OpenCode harness itself (executor, forwarder, typed client, app-server,
bridge, permissions, provider, ``omni opencode`` launcher, ap-web wiring,
``omni setup`` section, examples, and its test matrix) is unchanged. ruff
clean; opencode + registry + spec + dispatch suites green.
Co-authored-by: Isaac
* style(opencode): apply ruff format + prettier
Green the pre-commit (`ruff format`) and npm-test (`prettier --check`) CI gates:
- ruff format: opencode_native.py, opencode_native_provider.py,
test_host_opencode_native_e2e.py, test_opencode_auth.py (line-wrapping only).
- prettier: ap-web/src/lib/nativeCodingAgents.ts.
Formatting only — no behavior change.
Co-authored-by: Isaac
* fix(opencode): recover native-server coverage + fix enqueue harness-id
The split removed tests/harness_conformance/, which had been the coverage for
the *kept* native-server runtime (native_server_harness.py +
opencode_http_transport.py), dropping total coverage below the CI gate. Add
focused, Front-E-free unit tests:
- tests/test_native_server_harness.py — drives the transport-agnostic base over
an in-memory fake transport (run-turn boot-poll / model pin / error branches,
interrupt, enqueue, capabilities).
- tests/test_opencode_http_transport.py — the prompt-payload builder + every
transport method over an injected fake OpenCodeClient.
The base test caught a real regression from the descriptor de-coupling: the
enqueue-failure path still referenced the removed ``self.descriptor.id`` (an
AttributeError on that error branch) — now ``self._harness_id``.
Co-authored-by: Isaac
* feat(opencode): pick a default model from `omni setup`
`omni opencode` spawns `opencode serve` with a per-session XDG config (the
user's global ~/.config/opencode is intentionally ignored), so with no model
configured opencode falls back to its built-in default (opencode/big-pickle)
even after `opencode auth login` adds a provider. Add a way to choose the
launch model:
- `omni setup` → OpenCode → "Set default model": lists `opencode models`,
persists the pick as the `opencode_model` global-config key (+ a Clear
option). New helpers `_list_opencode_models` / `_set_opencode_default_model`.
- `omni opencode` (no --model) now prefers `opencode_model`, falling back to the
shared `model` key for back-compat.
- Runner: write the resolved model into the per-session opencode.json at spawn
(build_opencode_model_default_config) so the TUI and the first turn launch on
it, not big-pickle — for both the user-provider and Databricks-gateway paths.
- Register `opencode_model` in `_GLOBAL_CONFIG_KEYS` so `omni config` accepts it.
Also registers the `opencode` command in `_CLICK_SUBCOMMANDS` (it was registered
on the CLI group but unreachable from main(), which failed
test_click_subcommands_allowlist_covers_registered_commands).
+ unit tests (provider helper, model picker persist/clear/cancel/empty).
Co-authored-by: Isaac
* test(opencode): cover the `omni opencode` launcher helpers
opencode_native.py (the `omni opencode` launcher) had no direct unit tests —
556 lines of spec-materialization, payload parsing, tmux-attach gating, and
httpx session/terminal helpers sitting uncovered (the biggest single coverage
sink in the harness, and part of why dropping the well-covered Front E modules
pushed total coverage under the gate).
Add tests/test_opencode_native.py covering the unit-testable surface over a
fake AsyncClient: `_materialize_opencode_agent_spec` (model on/off),
`_launched_opencode_terminal_from_payload`, `_direct_tmux_unavailable_reason`,
`_resolve_session_id_for_resume`, and the session/terminal helpers
(`_create_opencode_session`, `_fetch_opencode_session`,
`_ensure_opencode_terminal_on_runner`, `_find_running_opencode_terminal` incl.
404 / not-running / offline-runner branches). Launcher coverage 0% → 56%; the
daemon/tmux attach plumbing stays for the live host e2e.
Co-authored-by: Isaac
* test(opencode): smoke-test the opencode-native harness create_app/factory
inner/opencode_native_harness.py (the `harness: opencode-native` entry point)
was at 0% — add a create_app() FastAPI smoke test + an executor-factory test
(builds OpenCodeNativeExecutor from the spawn env). 0% -> 100%.
Co-authored-by: Isaac
* fix(opencode): seed user auth into the session server so the chosen model works
The runner spawns `opencode serve` with a per-session XDG_DATA_HOME (isolating
session state), which also hid the user's `opencode auth login` credentials
(~/.local/share/opencode/auth.json). Without them the server could only reach
OpenCode's no-auth default (opencode/big-pickle), so `omni opencode` ignored
the selected provider/model — even with the model pinned into opencode.json.
- bridge: `seed_opencode_auth()` copies the user's auth.json into the
per-session XDG_DATA_HOME at spawn (0600, refreshed each launch); the runner
calls it before `opencode serve` starts. No-op on a remote runner / the
Databricks-gateway path (no local auth.json).
- setup: the "Set default model" picker listed every models.dev model
(hundreds) — overflowing the menu viewport and flickering. Filter to models
whose provider the user can authenticate (stored auth.json + env keys) via
the new `reachable_provider_ids()`; fall back to the full list only if that
filter would hide everything.
+ tests (auth-seed copy/no-op, reachable provider ids).
Co-authored-by: Isaac
* fix(setup): scrolling viewport for the OpenCode model picker (no more flicker)
The model picker still flickered when the reachable-provider model list was
longer than the terminal: select() rendered every row and redrew in place, so a
frame taller than the screen overflowed and flickered.
Add an opt-in scrolling viewport to select(max_visible=...): when set and the
list is longer, it renders only a window of rows that follows the cursor (with
"↑ N more" / "↓ N more" markers), bounding the frame to one screen. Default
(None) renders every row, so all other menus are unchanged. The OpenCode "Set
default model" picker sizes the viewport to the terminal height.
+ tests for the windowed vs full render.
Co-authored-by: Isaac
* test(opencode): raise coverage — test tractable gaps + pragma e2e-only orchestration
The split dropped Front E's well-covered code, dipping total coverage past the
code-coverage ratchet's 0.5% tolerance. Recover it honestly — real unit tests
for the testable surface, and `# pragma: no cover` only on integration-only
orchestration that the live host e2e exercises but unit tests can't.
Unit tests:
- launcher: _preflight_local_tools, _update_startup_progress,
_direct_tmux_unavailable_reason (tmux-missing / all-present),
_wait_for_opencode_terminal_ready (found / timeout).
- app-server: find_opencode_cli (absolute exe) + resolve_opencode_version
(parse / run-error / unparseable).
- client: error + edge branches (non-object bodies, HTTP errors).
- forwarder: seed_dedupe_from_history (resume seeding + best-effort failure).
pragma (e2e-covered, not unit-testable — see tests/e2e/test_host_opencode_native_e2e.py):
- launcher daemon/tmux flow: run_opencode_native, _run_with_remote_server,
_prepare_opencode_terminal_via_daemon, _attach_terminal_resource,
_attach_direct_tmux, and the SDK resume picker.
- OpenCodeNativeServer.close().
Co-authored-by: Isaac
---------
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
|
||
|
|
da5b06349a |
feat(harness): add goose-native harness (Block's Goose CLI) (#823) (#955)
* feat(goose): register goose-native harness (#823) Additive registration mirroring cursor-native: aliases, wrapper label, NativeCodingAgent metadata, harness module map, spec validation, and terminal role. No behavior yet; the harness module lands in later units. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(goose): native executor, harness, and tmux bridge (#823) GooseNativeExecutor injects each web-UI turn into the running `goose session` TUI's tmux pane (no output streaming; supports mid-turn steering); goose_native_harness exposes create_app(); goose_native_bridge owns the tmux target handshake + bracketed-paste injection (single Enter) + spawn env (GOOSE_CLI_THEME=ansi, GOOSE_PROVIDER/MODEL). Mirrors cursor-native; drops the .cursor/mcp.json machinery (Goose MCP lives in config.yaml). Readiness uses a stable-pane settle since Goose has no sentinel prompt. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(goose): session-store forwarder (#823) Tail Goose's SQLite session store (~/.local/share/goose/sessions/ sessions.db): resolve the session by the --name we launched with, poll messages past a monotonic id cursor, decode content_json (tolerant of str/list/dict part shapes), and POST new user/assistant rows as external_conversation_item. Persists the high-water id for restart-safe resume; supervisor restarts with bounded backoff. Verified against the real schema + a fixture (Goose 1.38.0). Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(goose): runner wiring + CLI launch orchestration (#823) Runner: _auto_create_goose_terminal launches `goose session --name <id>` in a tmux pane (GOOSE_CLI_THEME=ansi), advertises the tmux target for the harness executor, and starts the session-store forwarder; spawn-env branches, ensure-locks, interrupt/stop handlers, status suppression, and cleanup all mirror cursor-native. goose_native.py owns the `omni goose` CLI orchestration (resolve binary, create/resume session, daemon bind, terminal-ready poll, direct tmux attach). Mirrors cursor, minus MCP. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(goose): omni goose CLI command, resume dispatch, onboarding readiness (#823) Add the `omnigent goose` command (mirrors `omnigent cursor`: --server/ --resume/--session + raw goose args, daemon-spawned runner, tmux attach), register it in _CLICK_SUBCOMMANDS, route `omnigent resume` to run_goose_native for goose-native sessions, and teach onboarding to gate goose-native readiness on the `goose` binary (install hint: brew install block-goose-cli). Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(goose): onboarding readiness/config reporter (#823) goose_auth.py is a read-only reporter (Omnigent manages no Goose credentials — Goose owns its auth via `goose configure`): confirms the `goose` binary and surfaces the configured provider/model (env overrides config, matching Goose's precedence) for setup display. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(goose): web UI Goose icon + native-agent wiring (#823) Add GooseIcon (lobehub Goose glyph), register goose-native in the native-coding-agent registry (icon kind, harness alias, sort rank), widen the icon-kind unions, and resolve the Goose glyph in AgentCard + SubagentsPanel. Extends AgentCard tests with goose cases. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * test(goose): unit + e2e coverage for goose-native harness (#823) Unit tests for the forwarder (fixture DB matching the verified Goose 1.38 schema: discovery-by-name, content_json decode, attachment strip, role mapping, idempotent cursor), spawn env, executor injection, CLI resolve, and onboarding reporter — 25 tests, all green. Plus an opt-in e2e (OMNIGENT_E2E_GOOSE_NATIVE=1) smoke + cwd test mirroring cursor-native, skip-gated when goose/tmux are absent. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(goose): suppress first-run telemetry prompt in the terminal (#823) Live e2e surfaced that a fresh Goose install blocks the headless pane on its interactive "share usage data?" prompt. Set GOOSE_TELEMETRY_OFF=1 on the goose terminal env (alongside GOOSE_CLI_THEME=ansi) so the first-run prompt never gates message injection. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * style(goose): wrap _message_to_item signature to satisfy ruff E501 (#823) Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(goose): harden forwarder binding + lifecycle from codex/adversarial review (#823) Cross-model review (codex + adversarial subagent) converged on the forwarder's session binding and lifecycle: - Per-launch-unique goose session name (`<conv_id>-<ms>`): `goose session --name X` without --resume creates a NEW row each launch (verified, Goose 1.38), so the forwarder now binds to exactly this launch's row and can never replay an older same-conversation transcript on cold-resume. - Cancel the TUI->web forwarder on session teardown (was leaked): a deleted session no longer leaves a supervisor polling a dead store + POSTing forever. Covers cursor-native too (shared cleanup path). - Anchor the paste-confirm needle to the message's last line, not first, so on-screen echo of a prior turn can't trigger a premature Enter. - Surface persistent sqlite read errors once (deduped warning) instead of swallowing them into a silently-empty chat view. Re-verified live: goose-native e2e smoke + cwd still pass via OpenRouter. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * test(goose): add native goose render-parity e2e_ui test (#823) Mirror test_native_cursor_render_parity for goose-native: a native_goose_session fixture (auto-launches goose session on bind) + a render-parity Playwright test asserting composer-IN parity, a TUI-originated turn surfacing OUT via the forwarder, and no duplicate rendering. Skip-gated when goose/tmux/provider-config are absent (CI-safe). Satisfies the E2E UI Required gate for the ap-web Goose icon change. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(goose): use os.environ.copy() in tmux attach to clear exfil-scan (#823) The exfil security-scan blocks the `dict(os.environ)` shape in added lines. os.environ.copy() is the identical plain-dict copy (drops TMUX before the local tmux attach) without tripping the wholesale-environ-dump pattern. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * style(goose): prettier-format ConversationIconKind union (#823) CI 'Check formatting' flagged the hand-wrapped union; prettier keeps it on one line (fits print width). Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * style(goose): apply pre-commit ruff-format (#823) Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(goose): include goose-native in configured_harness_map (#823) The harness-coverage meta-test caught a real gap: configured_harness_map() added _CURSOR_NATIVE_HARNESSES but not _GOOSE_NATIVE_HARNESSES, so the canonical 'goose-native' spelling was absent from the hello-frame readiness map (the web UI 'needs setup' warning would have missed it). Add it, and cover goose in the readiness test's spelling lists. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(goose): surface Goose in `omnigent setup` (configure harnesses) Wire onboarding/goose_auth.py (previously dead code) into the configure- harnesses menu: a "Goose" row that reports readiness (binary installed + provider configured via goose_config_summary) and a drill-in (_manage_goose_harness) that installs the CLI (brew/curl hint, non-npm) and launches `goose configure`. Goose owns its own auth (keyring / config.yaml), so Omnigent stores no key — mirrors the Qwen drill-in. Serves both the goose-native (TUI) and upcoming headless goose (ACP) harnesses. Adds 3 drill-in tests (missing-CLI hint, Back no-op, configure launch). Co-authored-by: Isaac * feat(goose): headless Goose ACP harness (GooseExecutor + wrap) Adds the chat-first `harness: goose` — the ACP counterpart to the terminal-first `goose-native` TUI. GooseExecutor drives `goose acp` over newline-delimited JSON-RPC 2.0 (initialize / session/new / session/prompt), streaming agent_message_chunk -> TextChunk and folding the system prompt into the first turn. Goose's mid-turn `session/request_permission` routes through Omnigent's generic TOOL_CALL policy + human-consent elicitation (ctx.elicit -> web ApprovalCard), so tool approvals surface as web elicitation cards rather than in-terminal prompts. Closes two qwen-harness gaps for Goose: token usage (TurnComplete.usage from the final result) and context window (max_context_tokens from usage_update). Modeled on QwenExecutor; verified end-to-end against a live goose 1.38 acp session (streaming + policy(ASK)->elicit->allow->tool-run + usage). goose_harness.create_app() wraps it via ExecutorAdapter (lazy build; provider/ model/cwd/builtins from HARNESS_GOOSE_* env). 19 unit tests. Co-authored-by: Isaac * feat(goose): register the headless `goose` harness across touchpoints Wires `harness: goose` into every registration site so it is runnable, selectable, and readiness-gated: - runtime/harnesses/__init__: goose -> omnigent.inner.goose_harness - workflow.AgentHarnessType += goose; new _build_goose_spawn_env (model + os_env only — Goose owns its auth via `goose configure`, so no gateway wiring; databricks-* models dropped) - runner/app: HARNESS_GOOSE_MODEL env key + spawn-env dispatch - onboarding/harness_install: goose -> GOOSE_KEY (gate on the goose binary) - onboarding/harness_readiness: headless goose gated on the binary + in the map - spec/_omnigent_compat: OMNIGENT_HARNESSES += goose (so --harness goose validates) - model_override: goose honors --model; cli: _OS_ENV_HARNESSES + help + prompt Tests: 3 _build_goose_spawn_env cases; configured_harness_map covers the new `goose` spelling. Co-authored-by: Isaac * feat(goose): web picker glyph for the headless goose harness The AgentCard harness fallback already maps any `harness` containing "goose" to GooseIcon, so a headless `harness: goose` agent renders with the Goose glyph in the new-session / add-agent pickers (better than qwen, which falls back to the bot icon). Adds a test case for the headless `goose` harness and refreshes the iconForAgent doc comment. Onboarding is served by the shared `omnigent setup` Goose row. Per-session brain-harness override (BRAIN_HARNESS_LABELS) is left for when Omnigent tools are exposed to Goose over ACP MCP, matching qwen. Co-authored-by: Isaac * test(goose): opt-in live e2e for the headless goose ACP harness tests/e2e/test_goose_acp_e2e.py drives GooseExecutor against a real `goose acp` process (isolated temp HOME, CI-safe skip behind OMNIGENT_E2E_GOOSE=1 + a configured provider): (1) a prose turn streams agent text and completes with token usage + a learned context window; (2) a shell tool call routes through policy(ASK) -> elicitation -> approve, then the tool runs and its marker reaches the transcript — the web ApprovalCard path. Both verified passing against goose 1.38 / claude-haiku-4-5. Co-authored-by: Isaac * fix(goose): web-UI duplicate, terminal switcher, and robust config detection Three fixes from live testing of the Goose harnesses: 1. Duplicate "Goose" in the new-chat picker: add "goose-native-ui" to NewChatDialog's BUILTIN_AGENTS so the server-persisted goose agent (created by `omnigent goose`) is deduped against the static NATIVE_CODING_AGENTS entry — matching claude/codex/cursor/pi. 2. Terminal view opened a plain shell and the Chat/Terminal pill vanished for native Goose: terminal_goose_main was missing from AGENT_TERMINAL_IDS, so goose's TUI pane wasn't recognized as the agent terminal (leaked into Shells, tripped isShellView). Add it — same omission/fix as the earlier pi/cursor regressions. Now goose-native switches chat<->terminal like the other natives. 3. `omnigent setup` showed Goose unconfigured even after `goose configure`: the old detector hand-parsed config.yaml for a top-level GOOSE_PROVIDER, which misses the keyring/format `goose configure` actually writes. Now detect via `goose info -v` (Goose's own resolved config — authoritative across platforms), with the file scan kept as a fallback when the binary can't be run. Tests: goose_info_config parse/precedence/fallback; useTerminals goose regression case; existing suites green (226 frontend, goose python). Co-authored-by: Isaac * chore(goose): snappier forwarder poll + lint/format + executor coverage - goose-native forwarder poll 0.7s → 0.4s: goose flushes a SQLite messages row per agentic step (verified), so a tighter cadence makes the mirrored chat track the terminal step-by-step on coding turns rather than lagging each one. - Apply ruff format/check across the goose modules (fixes Pre-commit CI). - Expand GooseExecutor unit tests (transport: _rpc/_read_stdout/_read_stderr, handshake/session lifecycle, _start_process reset, sandbox launch-path, run_turn boot-failure / ACP-error-reset / usage-update paths). Coverage 53% → 80%. Co-authored-by: Isaac * test(goose): cover goose_harness wrap + executor image/permission branches Lifts goose_executor + goose_harness coverage 80% → 89%: goose_harness was entirely uncovered (now ~95% — _resolve_os_env JSON/default/malformed, _build_goose_executor env reading + defaults, create_app), plus GooseExecutor branches for attachment/image handling (_inline_text_file_data variants, _image_blocks_from_content parse/SSRF-skip, image-marker toggle, run_turn image forwarding) and the _decide_permission edges (no-gates allow, ASK-without-handler deny, policy-exception fall-through, request-handler exception → JSON-RPC error). Co-authored-by: Isaac * test(e2e): exclude goose + goose-native from the live run-harness matrix test_run_harness_live_matrix_covers_registered_coding_harnesses asserts every registered coding harness has a live gateway round-trip row. Headless `goose` authenticates from its own `goose configure` config (no shared HARNESS_*_GATEWAY/DATABRICKS_PROFILE wiring — like qwen), and `goose-native` is a terminal-first TUI launched via `omni goose` (like claude-/cursor-native), so both are excluded from this gateway-driven matrix. Their live coverage lives in the dedicated test_goose_acp_e2e.py / test_goose_native_cli_e2e.py suites. Co-authored-by: Isaac * fix(ci): de-pollute ap-web/package-lock.json — drop databricks npm-proxy URL A merge carried a `resolved` URL pinned to the internal `npm-proxy.cloud.databricks.com` (the `yaml` dep) into the lockfile. `npm ci` fetches each package from its locked `resolved` URL regardless of NPM_CONFIG_REGISTRY, so every frontend CI job (pre-commit, npm test, UI Snapshot, E2E UI shards) failed at install with `ETIMEDOUT` against that internal proxy — which the public OSS CI can't reach. package.json is unchanged vs main, so the lock is restored to origin/main's clean state (all deps resolve from registry.npmjs.org). The npm analog of the uv.lock proxy-leak. Co-authored-by: Isaac --------- Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> Co-authored-by: praneeth_paikray-data <praneeth.paikray@databricks.com> Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com> |
||
|
|
ee1a604ed8 |
perf(runner): cache terminal is_alive() probe with short TTL (#924)
Rapid web-client polling of the terminal GET endpoint forks a tmux has-session subprocess on every request. Add a 2-second TTLCache so the probe runs at most once per terminal per TTL window, while still detecting dead tmux servers promptly. |
||
|
|
526703bc53 |
feat(cursor): add cursor-native harness (cursor-agent acp over stdio) (#551)
* feat(cursor): add cursor-native harness (cursor-agent acp over stdio)
Adds a `cursor-native` harness that drives the official Cursor CLI's Agent
Client Protocol server (`cursor-agent acp`) over stdio JSON-RPC — the
codex-native model, but stdio instead of a WebSocket. This is the core slice:
session create + prompt + streamed `session/update` mapped to ExecutorEvents.
Unlike the SDK `cursor` harness, auth is the ambient `cursor-agent login`
($HOME/.cursor) — no CURSOR_API_KEY. Despite the "native" name it behaves like
the SDK harness (streaming, runner replays history), so it is intentionally NOT
in NATIVE_HARNESSES.
- omnigent/inner/cursor_acp_client.py: async stdio JSON-RPC client for
`cursor-agent acp` (initialize / session.new / session.load / session.prompt /
session.cancel; handles agent->client request_permission + fs/* requests).
- omnigent/inner/cursor_native_executor.py: CursorNativeExecutor — streaming
executor; maps agent_message_chunk/agent_thought_chunk/tool_call(_update) to
Text/Reasoning/ToolCall events.
- omnigent/inner/cursor_native_harness.py: create_app() wrap.
- Registration: _HARNESS_MODULES, OMNIGENT_HARNESSES, runner spawn-env dispatch
+ _build_cursor_native_spawn_env.
- tests/inner/test_cursor_native_executor.py: unit tests for update mapping,
prompt building, capability flags, ACP request handlers, registration.
Deferred to follow-ups: MCP host-tool relay, session/request_permission ->
policy bridge, resume via session/load, per-session $HOME isolation, model pin.
Verified end-to-end locally:
omnigent run hello_world.yaml --harness cursor-native -p "..." -> streamed reply, exit 0.
Co-authored-by: Isaac
* fix(cursor): harden cursor-native ACP client + add deterministic client tests
Bug-bash follow-ups on the cursor-native (ACP) harness (8/8 live e2e scenarios
pass; an adversarial review surfaced the P0/P1s below).
cursor_acp_client.py:
- P0: answer agent->client requests (session/request_permission, fs/*) on a
separate task instead of awaiting the reply inline in the read loop. Replying
inline parks the reader in stdin.drain() while not draining stdout — if the
agent's stdout pipe is full it can't read our reply, a deadlock. Now the reader
keeps draining; close() cancels+awaits the request tasks.
- A failed reply-send (broken pipe / dead proc) is suppressed so it can't kill
the reader task as an unretrieved exception.
- close() now awaits the cancelled reader/stderr tasks (deterministic cleanup,
no "Task was destroyed but pending" warnings).
- prompt() pops its _prompt_session entry in a finally (no leak on early close).
- _dispatch guards a None message id.
cursor_native_executor.py:
- P0: on first-turn start failure, close the local client directly. It was not
yet stored in self._sessions, so close_session() popped nothing and the
cursor-agent acp subprocess + reader tasks orphaned.
- P1: derive is_first_turn from has_sent_prompt (not just session existence), and
build the prompt before spawning so an empty turn is a cheap no-op and never
drops first-turn system-prompt semantics.
P1 (model-override table sync): remove cursor-native from _HARNESS_MODEL_ENV_KEY
and stop threading HARNESS_CURSOR_NATIVE_MODEL. cursor-agent acp uses its
configured default and the executor ignores a model pin, so cursor-native is now
consistently absent from all three tables (incl. _SDK_MODEL_OVERRIDE_HARNESSES).
tests/inner/test_cursor_acp_client.py: deterministic tests driving the real
client against a stdlib-only fake ACP server — streaming, multi-turn isolation,
JSON-RPC error -> CursorAcpError, the agent permission round-trip (no deadlock),
EOF mid-turn, and subprocess cleanup. No cursor-agent/network needed.
Verified: 27 cursor-native unit tests pass; 299 existing tests across the edited
modules (spawn-env, model-override, aliases, cursor executor/harness, runner
dispatch) pass; ruff clean.
Co-authored-by: Isaac
* feat(cursor): omnigent cursor launches the Cursor TUI in an omnigent terminal
Branch B, Stage 1: adds the `omnigent cursor` verb that launches cursor-agent's
interactive TUI inside an omnigent-runner-owned tmux terminal and attaches the
local TTY — the cursor analog of `omnigent codex` / `omnigent pi`.
Mirrors the pi-native template (simplest TUI launcher; no app-server, no
forwarder): create/resume session -> daemon runner bind -> POST ensure terminal
{terminal: "cursor"} -> runner spawns `cursor-agent` in tmux -> direct tmux
attach. Auth is the ambient `cursor-agent login` ($HOME inherited), so no API
key and no extension bridge.
- omnigent/cursor_native.py: run_cursor_native + the daemon/terminal/attach flow.
- omnigent/cli.py: `omnigent cursor` verb (+ _CLICK_SUBCOMMANDS).
- omnigent/runner/app.py: _auto_create_cursor_terminal (launch cursor-agent TUI),
create_session dispatch, ensure-native-terminal route, ensure-lock, cleanup.
- registration: _wrapper_labels (CURSOR_NATIVE_WRAPPER_VALUE), native_coding_agents
(CURSOR_NATIVE_CODING_AGENT — UI-visible), harness_aliases (NATIVE_HARNESSES),
resource_registry (CURSOR_NATIVE_TERMINAL_ROLE), resume_dispatch.
cursor-native is now a terminal-native harness (in NATIVE_HARNESSES), so the
runner treats it like the other native TUIs. Flipped the Branch-A test that
asserted otherwise.
Verified live: `omnigent cursor --server <local>` creates the session, the runner
launches `cursor-agent` in tmux (`terminal_cursor_main` running, status bar wired
to the conversation link), and the CLI attaches (only fails to attach in a
non-TTY shell). 77 unit/registry tests pass; ruff clean.
Stage 2 (follow-up): mirror the TUI conversation to the web UI (read cursor's
store/hooks) + inject web-UI messages into the running TUI.
Co-authored-by: Isaac
* feat(cursor): bridge web-UI chat to the running Cursor TUI via tmux injection
Branch B, Stage 2 (the bidirectional bridge): web-UI messages now inject into the
running cursor-agent TUI instead of a separate side-session, so the web chat box
and the TUI are connected. Since the web UI embeds the same tmux pane, a message
sent from the web appears in the TUI (local terminal + embedded web terminal),
and TUI activity shows in the web embedded terminal.
This replaces the Branch-A ACP executor (which spun up a separate `cursor-agent
acp` session the user never saw) with the claude/pi-native tmux-injection model:
- omnigent/cursor_native_bridge.py (new): per-session bridge dir + tmux.json;
inject_user_message (clear draft -> bracketed paste via load-buffer/paste-buffer
-> Enter, multi-line safe; accepts the first-run "Trust this workspace" modal);
build_cursor_native_spawn_env.
- omnigent/inner/cursor_native_executor.py: rewritten to inject the latest web-UI
message into the TUI pane (supports_streaming=False; live steering).
- omnigent/runner/app.py: _auto_create_cursor_terminal writes tmux.json after
launch; cursor-native spawn-env now carries the bridge dir (mirrors pi-native);
dropped the stale Branch-A spawn-env dispatch.
- Removed the now-superseded ACP client + its test; rewrote the executor test for
the injection model (content extraction, paste-payload encoding, bridge
round-trip, registration).
Verified live: `omnigent cursor --server <local>` launches the TUI; POSTing a
web-UI message to the session injects it into the pane ("→ WEBUI_INJECT_BANANA"
appears in the live Cursor TUI). 16 unit tests pass; ruff clean.
Follow-up: structured chat-bubble mirror (cursor's chat store is content-addressed
SQLite, not a tailable transcript) — the embedded terminal already shows output.
Co-authored-by: Isaac
* fix(cursor): wire Stop/interrupt, status badge, robust injection + attachments
Addresses the audited P1 control-plane no-ops + injection robustness (all verified
live against a real cursor-agent on a test server):
- Stop session no-op (audit P1): cursor-native had no branch in the runner's
stop_session dispatch, so the Stop button never killed the pane (terminal +
cursor-agent leaked). Added cursor_native_bridge.kill_session + a
_handle_cursor_native_stop handler (kill tmux session, tear down terminal
resource, publish idle, reclaim sub-agent entry) — mirrors claude-native.
- Interrupt no-op (audit P1): added cursor_native_bridge.inject_interrupt
(sends Escape — verified to stop a cursor turn) + _handle_cursor_native_interrupt,
wired into the interrupt dispatch. Stop button now cancels the in-flight turn.
- Working-status badge stuck (audit P1): added CURSOR_NATIVE_TERMINAL_ROLE to the
PTY watcher's emit_status set (cursor has no forwarder, so the watcher is its
only status source — like pi/claude).
- Dead-terminal silent message loss (my live finding): inject_user_message now
fast-fails with a clear error if the tmux session is gone, instead of polling a
dead pane for the full 30s and dropping the message silently.
- Probabilistic dropped message (audit P1): wait for the pasted text to render in
the pane before sending Enter (avoids the Enter being folded into the paste as a
newline), instead of a fixed sleep + blind Enter.
- Trust-modal keystroke spam (audit P2): the 'a' accept is now one-shot.
- Dropped attachments (my live finding): the executor's _content_to_text now
materializes input_image/input_file to disk and references them by path so
cursor-agent can read them, instead of silently discarding non-text content.
Verified live: normal/leading-slash/multiline injection land; Escape interrupts a
running turn; kill_session kills the pane; dead-pane injection raises in ~0s (was
30s + silent loss). 17 unit tests pass; ruff clean.
Co-authored-by: Isaac
* feat(cursor): register cursor-native in the ap-web frontend (icon, picker, branding)
Fixes the audited frontend-registry cluster (the root cause of cursor-native
sessions rendering wrong / not appearing as a first-class agent):
- ap-web/src/lib/nativeCodingAgents.ts: add the cursor entry (key/agentName/
harness/wrapperLabel/displayName Cursor/iconKind cursor/sortRank 40), widen
NativeCodingAgentIconKind to include 'cursor', and add the native-cursor alias.
This is the single root fix — isNativeWrapper, nativeDisplayNameForAgent, sort
rank, slash/model gating, and branding all key off this registry.
- CursorIcon.tsx (lobehub Cursor glyph) + cursor branches in AgentCard.tsx and
SubagentsPanel.tsx (both icon sites) + the SDK 'cursor' harness fallback.
- sidebarNav.ts: add 'cursor' to ConversationIconKind so getConversationIconKind
stays type-sound now that the registry emits iconKind 'cursor'.
- NewChatDialog.tsx: add cursor-native-ui to BUILTIN_AGENTS and 'Cursor' to
AGENT_DISPLAY_ORDER so a cursor agent groups with the built-ins (not last,
fallback-iconed, in the custom group).
- test mocks (test-setup.ts global + AgentCard.test.tsx) + new cursor icon-
selection cases.
forkHarness.ts intentionally left unchanged: cursor cannot carry fork history
(no resume-by-id), so it stays out of the history-carrying fork path — the
matching backend honesty fix follows. Type-check clean; 138 frontend tests pass.
Co-authored-by: Isaac
* feat(cursor): seed cursor-native as a default agent + document tool-policy non-coverage
- Seed cursor-native-ui as a built-in agent on server startup (_ensure_default_
cursor_agent + _build_cursor_native_bundle, mirroring claude/codex/pi). Without
this, cursor only appeared in GET /v1/agents after the `omnigent cursor` CLI
first registered it, so a stock deployment's picker never showed it. Verified:
a fresh server now lists cursor-native-ui.
- Document in the harness that Omnigent's PreToolUse/PostToolUse tool policies do
NOT apply to cursor-native (cursor-agent gates tools with its own in-TUI
approval), so operators don't assume deny-policies constrain a cursor session.
Co-authored-by: Isaac
* fix(cursor-native): mirror TUI conversation back to the web UI
The cursor-native harness only injected web→TUI; nothing mirrored the
running cursor-agent TUI's conversation back into the Omnigent session,
so the chat view stayed empty and the spinner dropped the instant a
message was sent. Four reported symptoms, one root cause (no forwarder)
plus a status-edge bug:
1. Working spinner vanished — run_turn returns TurnComplete immediately
after the tmux paste, and cursor-native was absent from the
_publish_turn_status suppression set, so the turn-lifecycle idle raced
ahead of and clobbered the PTY watcher's running. Add cursor-native to
the suppression set (parity with claude/pi); the PTY watcher is now the
sole status source.
2. Session title stuck at "Cursor" — title seeds only when an
external_conversation_item is persisted; the forwarder now posts the
first user message, seeding it.
3. No assistant output in the web conversation — fixed by the forwarder.
4. TUI-typed follow-ups never appeared in the web UI — fixed by the
forwarder.
New omnigent/cursor_native_forwarder.py polls cursor's content-addressed
SQLite chat store (~/.cursor/chats/<md5(cwd)>/<chat-id>/store.db),
reading role-bearing JSON blobs in rowid order (= conversation order) and
posting user (unwrapped <user_query>) and assistant text as
external_conversation_item events. Store discovery is by md5(cwd) + newest
chat created since launch, with a cross-workspace fallback; dedup is an
O(1) high-water rowid persisted to the bridge dir; a supervisor restarts
on crash with bounded backoff. The store MUST be opened mode=ro (not
immutable=1) — a live chat keeps its data in the -wal sidecar, which
immutable=1 ignores. Wired into _auto_create_cursor_terminal (host-spawned
sessions have no CLI to start it) and cancelled on session stop.
Verified end-to-end against a real cursor-agent: spinner tracks the TUI,
title populates, assistant replies and TUI-typed follow-ups both mirror to
the web conversation.
Co-authored-by: Isaac
* fix(cursor-native): harden forwarder discovery, state, and remote-deploy URL
Follow-up to the TUI→web forwarder, addressing issues found by an adversarial
multi-agent audit of the cursor-native flow (verified against the live server +
a headless-browser bug-bash). The headline TUI→web mirroring already works
end-to-end (user + assistant render live, spinner tracks the TUI, title seeds);
these are correctness/robustness fixes around it:
- Require RUNNER_SERVER_URL instead of silently defaulting to localhost:6767
(matches codex's _required_runner_env). The default made every mirror POST
miss on a remote deploy, leaving the web conversation empty.
- Canonicalize the workspace with os.path.realpath before launch + discovery so
the cursor TUI's cwd and the forwarder hash the SAME md5(cwd) — a symlink /
trailing-slash mismatch would hide the chat store.
- Make store discovery cross-talk-safe: bind the exact md5(cwd) dir, and fall
back to other workspace dirs ONLY when exactly one chat qualifies. Two
candidates (concurrent same-cwd sessions, or an unrelated workspace) now
return None and retry rather than risk mirroring the wrong conversation.
- Clear the persisted forward cursor when the terminal is re-created
(clear_cursor_bridge_state, mirrors codex's clear_bridge_state) so a stale
store_path/last_rowid can't make the new forwarder resume the wrong chat.
- Surface (log) state-write failures instead of silently swallowing them; the
in-memory cursor still prevents within-process re-posting.
- Strip the executor's injected "[Attached: <path>]" markers from mirrored user
text so bridge paths don't leak into web-UI bubbles.
- Forwarder Authorization now rides solely on the refresh-capable auth (no
static header snapshot that would expire mid-session).
Audit findings deliberately NOT changed, with rationale: per-blob response_id is
fine (itemsToBlocks renders per-item in arrival order, not grouped by
response_id — confirmed live); cursor tool-call mirroring is a separate feature
(tool calls live in binary protobuf blobs, not the JSON message blobs); the
shared native sub-agent-completion path and shared terminal idle markers were
left untouched to avoid regressing claude/codex/pi.
Tests: 3 new unit tests (ambiguous-discovery → None, attachment-marker strip,
state clear); all 22 cursor-forwarder tests pass.
Co-authored-by: Isaac
* fix(cursor): register cursor pane in AGENT_TERMINAL_IDS
The cursor-native agent's terminal pane has id ``terminal_cursor_main``
(``terminal_{terminal_name}_{session_key}`` with ``terminal_name="cursor"``),
but it was missing from the frontend ``AGENT_TERMINAL_IDS`` allowlist. That
made ``isShellView`` treat the agent's own terminal as a user shell, hiding
the Chat/Terminal toggle pill in Terminal view and stranding the user with
only the close affordance. The pane also leaked into the Shells inventory.
Add ``terminal_cursor_main`` to the set (mirroring the existing tui/claude/
codex/pi entries) and add regression tests in ``isAgentTerminalKey`` and
``inventoryTerminals`` matching the pi cases.
Co-authored-by: Isaac
* test(cursor): exclude cursor-native from gateway e2e harness matrix
cursor-native now lands in OMNIGENT_HARNESSES ∩ _HARNESS_MODULES, so
test_run_harness_live_matrix_covers_registered_coding_harnesses expected a
live HARNESS_PROBES row for it and failed. cursor-native can't round-trip
this gateway-backed matrix for the union of the existing exclusions: like
the *-native harnesses it needs a bridge dir + runner-managed tmux pane (set
up by ``omnigent cursor``, not ``omnigent run --harness cursor-native``), and
like ``cursor`` it drives cursor-agent against Cursor's own backend. Its live
coverage is the gated row in test_per_harness_cursor.py.
Co-authored-by: Isaac
* docs(cursor): correct stale cursor-native harness-registry comment
The registry comment still described the pre-pivot design (Cursor ACP server
over stdio, streaming executor, "intentionally absent from NATIVE_HARNESSES").
The shipped harness drives the resident cursor-agent TUI via tmux injection
and IS in NATIVE_HARNESSES. Align the comment with the implementation.
Co-authored-by: Isaac
---------
Co-authored-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: Serena Ruan <82044803+serena-ruan@users.noreply.github.com>
|
||
|
|
3d73b3e0e8 |
fix(runner): don't mark a chat failed when a native terminal exits while idle (#559)
* fix(runner): don't mark a chat failed when a native terminal exits while idle
A required native agent terminal (Claude Code / pi) is long-lived and goes
``idle`` once its turn completes. When its tmux pane later disappears, the
runner unconditionally published ``session.status: failed`` ("Required
terminal exited unexpectedly"), so chats whose work had already succeeded
showed up as failed in the UI whenever the terminal shut down cleanly.
Track the latest PTY-derived session status per session in the resource
registry and carry ``session_was_idle`` on ``TerminalExitEvent``. The runner
now suppresses the failure (only releasing the harness subprocess) when the
session was idle at exit, while a mid-turn exit (last status ``running``) and
a boot failure (no status observed) both still fail the session.
* docs(runner): tighten terminal-exit comments
* fix(runner): close turn-boundary window in native terminal-exit classification
Address PR review: the PTY-status memo was never reset at turn start, so a
crash in the window between a new turn beginning and the watcher's first
``running`` edge would read the prior turn's stale ``idle`` and be
misclassified as a clean shutdown — silently swallowing a real failure.
- Add ``note_session_turn_started`` and call it when a native session receives
a message, marking the session running until the watcher next sees idle.
- Funnel all memo access through lock-guarded helpers (thread safety).
- Guard ``transfer_terminal`` against clobbering the target's own status.
- Rename ``_release_failed_required_terminal_session`` →
``_release_required_terminal_session`` (it only releases the subprocess and
publishes no failure events, so it is safe on the clean-shutdown path).
- Add regression tests: crash after a new turn fails; cleanup/transfer memo.
* test(runner): fake launch in transfer-memo test so CI has no real codex process
test_transfer_terminal_moves_status_memo launched a real codex terminal, which
exits immediately in CI (no binary) → "terminal codex:main exited before it
became available". Mirror test_terminal_resource_role_moves_on_transfer:
monkeypatch the launch and conversation-link update so the test exercises only
the memo move.
|
||
|
|
b6dcd76549 |
fix(runner): handle required terminal lifecycle failures (#176)
* fix(runner): handle required terminal lifecycle failures Signed-off-by: Heather Miller <heather.miller@cs.cmu.edu> * fix(runner): launch pi-native terminal as required (lifecycle parity) The required/auxiliary terminal lifecycle rename updated the claude, codex, repl, and REST launch sites but missed _auto_create_pi_terminal, which still called the removed launch_terminal — an AttributeError the moment a pi-native session boots. Pi's terminal process is the session runtime, so it is required (parity with claude-native). Add a regression test exercising _auto_create_pi_terminal against a registry exposing only launch_required_terminal, so a stale call site fails in CI instead of in production. Co-authored-by: Isaac --------- Signed-off-by: Heather Miller <heather.miller@cs.cmu.edu> Co-authored-by: Dhruv Gupta <dhruv0811@gmail.com> |
||
|
|
3fe0cc1f62 |
Add native Pi TUI integration (#22)
* Add native Pi TUI integration * fix(pi-native): wire interrupt/stop, gate readiness, fix inbox ordering & interrupt cleanup Follow-up fixes from review of the native Pi integration: - runner/app: route pi-native `interrupt` and `stop_session` to `_handle_pi_native_interrupt`. Both branches enumerated only claude/codex native, so pi-native fell through to the in-process cancel floor (a no-op for native instant-turn harnesses) — clicking Stop on a Pi turn did nothing. The purpose-built handler existed but had no callers. - harness_readiness: gate `pi-native` on the `pi` CLI and expose it in `configured_harness_map`. `pi-native` had no `_HARNESS_FAMILY` entry (pi uses the `PI_SURFACE` sentinel), so it hit the unknown-harness fail-open branch — a missing `pi` CLI wasn't caught pre-spawn and the picker never warned. - pi_native_bridge: prefix inbox filenames with a monotonic ns timestamp + counter so the extension's lexicographic delivery matches enqueue order. uuid filenames carry no time order, and `interrupt_` sorted ahead of `msg_`. - extension: always consume an interrupt file after one delivery attempt. A non-actionable (idle) interrupt was left on disk, re-read every 250ms and could abort an unrelated later turn. The pendingInterrupt window still re-asserts the abort across a turn it actually caught. - tests: pi-native interrupt/stop dispatch routing, inbox ordering + atomic-write + 0o700 perms, and pi-native readiness gating. Co-authored-by: Isaac * fix(pi-native): drop removed TerminalEnvSpec kwarg that broke Pi terminal startup `_auto_create_pi_terminal` passed `tmux_show_conversation_link=False` to `TerminalEnvSpec`, but that field does not exist on the spec — the conversation link is now handled centrally by the terminal registry, and the claude/codex native terminal specs pass no such kwarg. Creating any pi-native session raised `TypeError: TerminalEnvSpec.__init__() got an unexpected keyword argument 'tmux_show_conversation_link'`, surfaced to the user as "Native Pi terminal failed to start; see runner logs for details" — so Pi could not launch at all. Remove the kwarg to match the claude/codex terminal specs. Verified end to end: creating a pi-native session now logs "Auto-created pi terminal" and the session goes idle with no task error. Co-authored-by: Isaac * fix(pi-native): register terminal_pi_main as an agent terminal (Chat/Terminal pill in Terminal view) `AGENT_TERMINAL_IDS` listed only `terminal_tui_main`/`terminal_claude_main`/ `terminal_codex_main`, so a native Pi session's own pane (`terminal_pi_main`) was treated as a *user shell*. In Terminal view that made `isShellView` true, so `ConnectionIndicator` hid the Chat/Terminal pill — the user was stranded in the terminal with no way back to Chat — and the pane leaked into the Shells inventory. Add `terminal_pi_main` to the allowlist. Co-authored-by: Isaac * chore(pi-native): satisfy CI — formatting, lint, openapi, readiness test Fixes the checks failing on the rebased PR: - ruff format: omnigent/pi_native.py, omnigent/runner/app.py - ruff check: import order in omnigent/repl/_resume_picker.py - prettier: ap-web/src/shell/SubagentsPanel.tsx - regenerate openapi.json (picks up the generalized native-terminal exemption wording in the session-terminal route docstring) - tests/onboarding/test_harness_readiness.py: expect pi-native / native-pi in configured_harness_map now that pi-native is gated like the pi surface Co-authored-by: Isaac * test(e2e_ui): cover native Pi picker label + terminal-first wrapper labels Adds the Playwright e2e_ui coverage the "E2E UI Required" gate asks for for the Pi native-agent UI. A start-session test that stubs the Pi agent and asserts: - the agent picker renders the harness-derived display label "Pi" (NOT the raw "pi-native-ui" — the regression the displayName mapping fixes), and - selecting Pi POSTs /v1/sessions with the terminal-first wrapper labels (omnigent.ui=terminal, omnigent.wrapper=pi-native-ui) that drive the runner-owned Pi TUI and the web Chat/Terminal view. Co-authored-by: Isaac --------- Co-authored-by: Sabhya Chhabria <sabhyachhabria@gmail.com> |
||
|
|
b3fb64710c | Welcome to omnigent |