The v1 line protocol (space-delimited commands) was removed on 2026-07-08. v2 JSON-RPC
is now the only socket protocol. Every consumer (CLI, shell integration, the debug/test
harnesses, and the automated test suites) was migrated to v2 first (see tests_v2/), and
a non-JSON line now gets a terse v1_removed error from TerminalController.processCommand
instead of being dispatched. The method mapping table below is kept for reference when
reading old scripts, commits, or bug reports that mention v1 command names.
Each request is one JSON object per line:
{"id":"1","method":"workspace.list","params":{}}Each response is one JSON object per line:
{"id":"1","ok":true,"result":{...}}Errors:
{"id":"1","ok":false,"error":{"code":"not_found","message":"workspace not found"}}Notes:
idis echoed back when present (string or number).- v2 methods accept IDs; v2 responses may include ephemeral
indexfields for ordering/debugging, but IDs are the stable handles. - Auth (
auth.login) is handled as a connection-level preamble before protocol dispatch, not a regular method — it worked the same way for v1 and v2 clients and was unaffected by the v1 removal.
System:
- ping ->
system.ping(liveness; the CLI'spingsubcommand calls this and prints "PONG" on success)
Windows:
- list_windows ->
window.list - current_window ->
window.current - focus_window ->
window.focus - new_window ->
window.create - close_window ->
window.close - move_workspace_to_window ->
workspace.move_to_window
Workspaces:
- list_workspaces ->
workspace.list - new_workspace ->
workspace.create - select_workspace ->
workspace.select - current_workspace ->
workspace.current - close_workspace ->
workspace.close
Worktrees (new in v2, no v1 predecessor -- see "worktree.* / layout.*" below):
- (no v1 equivalent) ->
worktree.create - (no v1 equivalent) ->
worktree.open - (no v1 equivalent) ->
worktree.remove - (no v1 equivalent) ->
worktree.list
Layouts (new in v2, no v1 predecessor -- see "worktree.* / layout.*" below):
- (no v1 equivalent) ->
layout.save - (no v1 equivalent) ->
layout.apply - (no v1 equivalent) ->
layout.list
Agent detection (new in v2, no v1 predecessor -- see "agent.detection.*" below):
- (no v1 equivalent) ->
agent.detection.list - (no v1 equivalent) ->
agent.detection.classify
Surfaces / Splits:
- list_surfaces ->
surface.list - focus_surface / focus_surface_by_panel ->
surface.focus - new_split ->
surface.split - new_surface ->
surface.create - close_surface ->
surface.close - drag_surface_to_split ->
surface.drag_to_split - refresh_surfaces ->
surface.refresh - surface_health ->
surface.health - trigger_flash ->
surface.trigger_flash(new in v2) - (no v1 equivalent) ->
surface.wait(new in v2, see "surface.wait" below) - (no v1 equivalent) ->
agent.prompt(new in v2, see "agent.prompt" below) - (no v1 equivalent) ->
subscribe/unsubscribe(new in v2, see "Socket Event Subscriptions" below) - (no v1 equivalent) ->
review.open/review.refresh/review.comment.add/review.comment.remove/review.comment.list/review.send_comments(new in v2, see "Review Panel" below)
Surface Telemetry (report_*/ports/git/pr — off-main parse, main.async mutate; see "Socket
command threading policy" in the root CLAUDE.md):
- report_tty ->
surface.report_tty - ports_kick ->
surface.ports_kick - report_pwd ->
surface.report_pwd - report_shell_state ->
surface.report_shell_state(shares theSocketFastPathStatededup helper) - report_git_branch ->
surface.report_git_branch - clear_git_branch ->
surface.clear_git_branch - report_pr / report_review ->
surface.report_pr - clear_pr ->
surface.clear_pr - report_ports ->
surface.report_ports - clear_ports ->
surface.clear_ports(omitsurface_idto clear all ports for the workspace)
Panes:
- list_panes ->
pane.list - focus_pane ->
pane.focus - list_pane_surfaces ->
pane.surfaces - new_pane ->
pane.create
Input:
- send / send_surface ->
surface.send_text - send_key / send_key_surface ->
surface.send_key
Sidebar Metadata (workspace-scoped — the set_status/log/set_progress/sidebar_state family
mutates a Tab/Workspace, not a specific surface; mutations are off-main + main.async,
reads use v2MainSync like sibling read methods):
- set_status ->
workspace.set_status - clear_status ->
workspace.clear_status - list_status ->
workspace.list_status - log ->
workspace.log - clear_log ->
workspace.clear_log - list_log ->
workspace.list_log - set_progress ->
workspace.set_progress - clear_progress ->
workspace.clear_progress - sidebar_state ->
workspace.sidebar_state - clear_agent_pid ->
workspace.clear_agent_pid - set_agent_pid ->
workspace.set_agent_pid - report_meta / clear_meta / list_meta -> alias of
workspace.set_status/clear_status/list_status - report_meta_block ->
workspace.report_meta_block - clear_meta_block ->
workspace.clear_meta_block - list_meta_blocks ->
workspace.list_meta_blocks - reset_sidebar ->
workspace.reset_sidebar - read_screen ->
surface.read_text(already covered read_screen's scrollback/lines semantics)
Notifications:
- notify ->
notification.create - notify_surface ->
notification.create_for_surface - notify_target ->
notification.create_for_target - list_notifications ->
notification.list - clear_notifications ->
notification.clear(accepts an optionalworkspace_idto scope the clear to one workspace, matching v1's--tab=X; omitting it clears all notifications, matching v1's bareclear_notifications) - set_app_focus ->
app.focus_override.set - simulate_app_active ->
app.simulate_active - reload_config ->
app.reload_config
Browser:
- open_browser ->
browser.open_split - navigate ->
browser.navigate - browser_back ->
browser.back - browser_forward ->
browser.forward - browser_reload ->
browser.reload - get_url ->
browser.url.get - focus_webview ->
browser.focus_webview - is_webview_focused ->
browser.is_webview_focused
Debug / Test-only:
- set_shortcut ->
debug.shortcut.set - simulate_shortcut ->
debug.shortcut.simulate - simulate_type ->
debug.type - activate_app ->
debug.app.activate - is_terminal_focused ->
debug.terminal.is_focused - read_terminal_text ->
debug.terminal.read_text - render_stats ->
debug.terminal.render_stats - layout_debug ->
debug.layout - bonsplit_underflow_count/reset ->
debug.bonsplit_underflow.* - empty_panel_count/reset ->
debug.empty_panel.* - focus_notification ->
debug.notification.focus - flash_count/reset ->
debug.flash.* - panel_snapshot/panel_snapshot_reset ->
debug.panel_snapshot.* - screenshot ->
debug.window.screenshot
Server-owned, event-driven wait on a single surface: block the connection (with a timeout)
until a condition is met, instead of the caller polling surface.read_text in a loop. One
request, one response — no subscription/unsubscribe pair to manage. CLI: programa wait-surface.
Request params (exactly one of pattern / exit / agent_state is required):
| Field | Type | Required | Notes |
|---|---|---|---|
workspace_id / surface_id |
string (id or ref) | no | Same resolution as other surface.* methods; defaults to the current workspace's focused surface. |
pattern |
string | one of pattern/exit/agent_state |
Regex (ICU/NSRegularExpression syntax) matched against the surface's current text (screen + scrollback) on every check. |
exit |
bool | one of pattern/exit/agent_state |
Wait for the surface's child process to exit. |
agent_state |
string | one of pattern/exit/agent_state |
Wait for the surface's #164 agent activity state to reach idle, working, blocked, or transition at all (any_change). |
timeout_ms |
int | no | Default 30000. Also accepts timeout (same units) for convenience. |
lines |
int | no | Caps how many trailing lines of scrollback pattern rereads per check (default 2000); does not apply to exit/agent_state. |
Response (ok: true):
{
"id": "1",
"ok": true,
"result": {
"workspace_id": "...", "workspace_ref": "workspace:1",
"surface_id": "...", "surface_ref": "surface:2",
"window_id": "...", "window_ref": "window:1",
"condition": "pattern",
"waited": true,
"match": "BUILD SUCCEEDED"
}
}waited: falsemeans the condition was already true the instant the call arrived (a marker already present in scrollback, the surface had already exited, or its agent_state already satisfied the requested condition) — the caller never actually blocked.matchis only present forpatternwaits and is the substring the regex matched.stateis only present foragent_statewaits and is the observed value ("idle","working","blocked", ornullfor "no state reported") at resolution.sourceis only present foragent_statewaits and is the additive sibling (docs/plans/screen-manifest-detection.md) telling you who reportedstate:"hooks"(a lifecycle hook — Claude Code/Codex/OpenCode) or"inferred"(the screen-manifest detection engine pattern-matching the terminal buffer for agents with no installed hooks).nulliffstateisnull. Hooks always win: a surface's source only ever reads"inferred"if no hook has ever reported for it. Manifest schema/authoring guidance for the"inferred"tier:docs/agent-detection-manifests.md.- Timeout:
{"ok": false, "error": {"code": "timeout", "message": "...", "data": {"timeout_ms": N}}}. - If the target surface doesn't exist (or, for
pattern/agent_state, is closed while the wait is in flight), the response isnot_found, not a timeout — callers shouldn't have to wait out the full timeout to learn the surface is gone.
idle— the surface's agent_state isidle, or has never been reported at all. Most terminals never report anything (no agent hooks installed), and a caller waiting for "idle" almost always means "not currently busy" — which is true of a bare terminal too. This is the one asymmetry versusworking/blocked, which both require an actual explicit report; there is nothing to observe in the no-state case for those.working— the surface's agent_state isworking.blocked— the surface's agent_state isblocked(a hook reported a permission/approval/ question prompt — see #164).any_change— resolves on the next agent_state transition after the call arrives (including a transition to "no state" on clear/reset). Never counts as already-satisfied at registration time, even if the state happens to already differ from some caller-assumed baseline — there is nothing to compare against until a transition is actually observed.
Backpressure / disconnect: surface.wait holds its socket connection's dedicated thread for up
to timeout_ms (each connection is handled on its own thread — see TerminalController. handleClient — so this never blocks other connections or the main thread). If the client
disconnects mid-wait, the in-flight write() of the eventual response simply fails and is
ignored; for the exit/agent_state conditions the registered watcher (in
SurfaceExitWaitRegistry / AgentStateWaitRegistry) still fires and is still cleaned up (the
callback is a fire-and-forget semaphore signal with nothing left to notify). There is no
server-side cap on concurrent waits; each is an independent blocked thread.
No-missed-events guarantee: the "is the condition already true?" check and "install the watcher"
step happen inside the same synchronous main-thread hop (v2MainSync), so a pattern marker
printed the instant the call arrives, a child-exit racing the call, or an agent_state change
racing the call, is always observed — see the doc comments on SurfaceExitWaitRegistry,
AgentStateWaitRegistry, and TerminalController.v2SurfaceWait in
Sources/TerminalController+SurfaceWait.swift. The pattern condition itself is polled
internally (Ghostty doesn't expose a push-based "surface content changed" callback the way it
does for child-exit) at a fixed ~100ms interval on the connection's own thread; exit and
agent_state are fully event-driven with no polling — exit off the same
GHOSTTY_ACTION_SHOW_CHILD_EXITED action that already closes the panel on process exit,
agent_state off the single main-thread mutation point for Workspace.panelAgentStates
(Workspace.updatePanelAgentState/clearPanelAgentState, always called from
TerminalController+Telemetry.swift's v2SurfaceReportAgentState/v2SurfaceClearAgentState via
DispatchQueue.main.async).
Submit a prompt to an agent surface and wait for it to finish, in one request — built on
surface.send_text's injection path plus surface.wait's agent_state condition. CLI: programa prompt-agent.
Request params:
| Field | Type | Required | Notes |
|---|---|---|---|
workspace_id / surface_id |
string (id or ref) | no | Same resolution as other surface.* methods; defaults to the current workspace's focused surface. |
text |
string | yes | The prompt. Enter is always submitted after it (trailing whitespace/newlines in text are trimmed first, so this never sends a stray blank line) — callers don't pass their own line ending. |
timeout_ms |
int | no | Overall budget for the agent to finish. Default 120000. Also accepts timeout. |
working_grace_ms |
int | no | How long to wait for the agent to report it started working before giving up on observing that transition. Default 3000. |
Response (ok: true):
{
"id": "1",
"ok": true,
"result": {
"workspace_id": "...", "workspace_ref": "workspace:1",
"surface_id": "...", "surface_ref": "surface:2",
"window_id": "...", "window_ref": "window:1",
"working_observed": true,
"final_state": "idle"
}
}warning is present (success, not an error) when the surface never reported any agent_state at
all — neither before the prompt was sent nor during the grace window — which usually means agent
hooks were never installed for this surface.
- Send + register, atomically. The text is sent via the exact same path
surface.send_textuses, and — inside that same main-thread hop — the surface'sagent_stateat that instant is captured and a watcher for the nextworkingtransition is registered viaAgentStateWaitRegistry. This closes the race where a hook reacts to the injected text before a separately-registered watcher would exist (the same atomic check+register patternsurface.waituses). - Grace window (
working_grace_ms). Wait for theworkingtransition.- Observed → go to step 3.
- Not observed by the time the grace window elapses → there is nothing further useful to
wait for, so resolve immediately using whatever
agent_statethe surface already has (working_observed: false). This is deliberately not a hard error: a prompt can finish faster than the grace window, or a hook simply may not fire for a trivial prompt.
- Wait for idle. Having observed
working, wait — for the remaining overalltimeout_msbudget — foragent_stateto reachidle(the same no-state-is-idle rule assurface.waitapplies: a hook that clears its own state on session end also counts). Resolves withworking_observed: true. - Timeout. If step 3's wait exceeds the remaining budget, the call fails with
{"code": "timeout", ...}— the agent started working but never finished in time.
If the surface was already working when the prompt was sent (e.g. a second prompt queued while
one is running), step 2 is skipped entirely and the call goes straight to step 3.
Agent diff review panel: a panel beside a terminal surface showing its worktree diff
(uncommitted changes, or vs a branch merge-base), with line comments that get sent back into
the terminal's input as path:start-end — text. CLI: programa review open|refresh|comment|send.
Opens a review panel split beside a terminal surface. Mirrors markdown.open's param/response
shape, plus a focus param (default false — this command never activates the app or moves
focus unless the caller explicitly opts in).
| Field | Type | Required | Notes |
|---|---|---|---|
surface_id |
string (id or ref) | no | Source terminal surface to review + split from. Defaults to the focused surface. Its panelDirectories entry supplies the git working directory. |
workspace_id / window_id |
string | no | Same resolution as other surface.*/markdown.* methods. |
mode |
string | no | "uncommitted" (default) — diff vs HEAD, includes uncommitted changes. "branch" — diff vs merge-base with base_branch (falls back to local main, then master, if the requested base doesn't resolve). |
base_branch |
string | no | Only used when mode: "branch". Default "origin/main". |
direction |
string | no | Split direction, default "right". |
focus |
bool | no | Default false. Only when true does this command activate the app/window and move pane focus to the new panel. |
Response (ok: true): window_id/_ref, workspace_id/_ref, pane_id/_ref, surface_id/_ref (the new
review panel), plus source_surface_id/_ref (the terminal being reviewed), mode,
base_branch, file_count, and diffable_file_count.
Errors: not_found (no focused surface / source surface not found), invalid_params (bad
mode/direction), unavailable (resolved directory isn't inside a git worktree).
Re-runs the diff for an existing review panel. surface_id (the review panel's own id) or
workspace-focused-review-panel fallback. Response: {"file_count": N, "diffable_file_count": M, "generated_at": <unix ts>}. Never activates the app or moves focus.
Pure in-memory comment metadata on a review panel, addressed by surface_id (the review panel).
add takes file_path (repo-relative), start_line/end_line (1-based, new-file line
numbers), text -> {"comment_id": "..."}. remove takes comment_id -> {"ok": true}.
list -> {"comments": [{"id", "file_path", "start_line", "end_line", "text", "created_at", "is_stale"}]} — is_stale is true when a refresh could no longer find that file/line range
(the comment is never dropped, only flagged).
Serializes every pending comment (path:start-end — text, one per line, file-then-line order)
and sends it into the source terminal surface via the same path surface.send_text uses (with
Enter submitted after, like agent.prompt), then clears the sent comments.
| Field | Type | Required |
|---|---|---|
surface_id |
review panel id/ref | yes (or workspace-focused fallback) |
preamble |
string | no — override the default "Code review comments" preamble |
Response (ok: true): {"sent_count": N, "target_surface_id"/"_ref": "..."}. Sending zero
pending comments is a no-op (sent_count: 0), not an error.
subscribe upgrades the calling connection to receive pushed events — agent state changes
(#164), coalesced surface output, and workspace lifecycle changes — instead of the caller
polling surface.list/workspace.list. Written before implementation, per the issue's ask that
backpressure/disconnect semantics be specified up front.
This is not the front door. surface.wait (one-shot conditions) and agent.prompt (submit +
wait) cover the common "wait for one thing" case in a single request/response round trip with no
queue/backpressure/reconnect model to manage. subscribe is for consumers that need many
events over time — dashboards, orchestrators, the menu bar of another machine — so a casual
caller never has to touch subscription machinery. CLI: programa watch-events (also positioned
as the advanced path, not the default way to wait for something).
| Field | Type | Required | Notes |
|---|---|---|---|
classes |
array of string | yes | Any of agent_state, output, workspace_lifecycle. Non-empty. |
surface_ids |
array of string (id or ref) | required iff classes includes output |
output is opt-in per surface — broadcasting every surface's output to every subscriber by default would be prohibitively expensive. |
Response (ok: true): {"subscription_id": "...", "classes": [...], "surface_ids": [...], "max_queued_events": 256}. A second subscribe on the same connection replaces the first
subscription (a connection has at most one).
No params. Tears down any live subscription on the calling connection; ok: true even if there
wasn't one, so a client doesn't need to track whether it subscribed.
Pushed events are not wrapped in the usual {"id", "ok", "result"} v2 envelope (there is no
request they're a response to) — each is its own single-line JSON object with an "event" key:
{"event": "agent_state", "workspace_id": "...", "surface_id": "...", "state": "working", "source": "hooks", "ts": 1737590400.123}
{"event": "output", "workspace_id": "...", "surface_id": "...", "text": "new terminal output...", "ts": 1737590400.223}
{"event": "workspace_lifecycle", "kind": "renamed", "workspace_id": "...", "title": "new title", "ts": 1737590400.323}
{"event": "dropped", "count": 7}agent_state.stateisnullfor a transition to "no state" (clear/reset) — same value shape assurface.wait'sagent_stateresult.agent_state.sourceis the same additive"hooks" | "inferred" | nullsibling documented undersurface.wait'sagent_statecondition above (screen-manifest detection, docs/plans/screen-manifest-detection.md) —nulliffstateisnull.output.textis the newly appended tail since the last tick for that surface, capped at 4000 characters — never the full buffer, and never per-byte (see Backpressure below).workspace_lifecycle.kindiscreated,closed, orrenamed.renamedfires only from the explicit rename entry point (tab-bar rename,workspace.rename) — not from automatic shell-title updates, which would otherwise flood subscribers on everycd.
Each subscription owns a bounded, drop-oldest queue of 256 events, drained on its own
dedicated serial dispatch queue — never on the thread that mutated app state (agent-state
mutation, output polling), so a slow or blocked client socket write never stalls a telemetry
mutation or the main thread. When the queue is full and a new event arrives, the oldest queued
event is evicted to make room (a client that's falling behind gets the freshest events, not a
growing backlog). Every time this happens, a {"event": "dropped", "count": N} frame is spliced
in ahead of the next real frame, so a client that sees one knows it may have missed events and
should re-sync current state with surface.list/workspace.list (frames carry surface_id/
workspace_id specifically so that re-sync is cheap and targeted).
output events are coalesced on a shared ~100ms poll tick across all watched surfaces (see
TerminalController.v2StartOutputPollLoopIfNeeded) — reusing the same point-in-time text read
surface.wait's pattern condition uses, since Ghostty doesn't expose a push-based
"content changed" callback at the app layer. One thread total drives this regardless of
subscriber/surface count, and it is a no-op tick whenever nothing is being watched.
The first failed write() (client gone) tears the subscription down immediately and unregisters
it from the broadcaster — no further events are enqueued for it. A connection's read loop
(TerminalController.handleClient) also unconditionally tears down any attached subscription
before closing the socket, covering client-initiated close and app shutdown alike. There is no
server-side cap on concurrent subscriptions.
Native git worktree workflow + named layout configs. Both new in v2 (no v1 predecessor). All
seven methods parse/validate params off-main; the actual git/filesystem I/O runs inside
GitWorktreeManager/ProgramaLayoutStore (blocking by construction, same pattern as
GitMetadataProber); v2MainSync is used only around the final tabManager.addWorkspace /
closeWorkspace mutation, matching workspace.create's existing shape. None of the seven steal
focus by default — worktree.create and worktree.open accept an opt-in focus: bool (default
false), gated through the existing v2FocusAllowed()/focusIntentV2Methods mechanism.
| Field | Type | Required | Notes |
|---|---|---|---|
repo |
string | yes | Any directory inside the repo; resolved via git rev-parse --show-toplevel. |
branch |
string | yes | Checked out if it already exists locally; otherwise created from base. |
base |
string | no | Ref to branch a new branch from. Default HEAD. Ignored if branch already exists. |
path |
string | no | Default <worktrees.directory>/<repo-name>/<branch-slug> (worktrees.directory setting, default ~/.programa/worktrees). |
layout |
string | no | Name of a saved layout (layout.save) to apply into the new workspace, baseCwd = the worktree root. |
focus |
bool | no | Default false. |
Result: {"worktree": {"path", "branch", "repo"}, "workspace_id", "workspace_ref", "window_id", "window_ref"}.
Errors: not_a_git_repo, branch_checked_out (pre-detected via git worktree list --porcelain,
not git's error text), worktree_path_exists, layout_not_found, git_command_failed.
| Field | Type | Required | Notes |
|---|---|---|---|
repo |
string | yes | Same resolution as worktree.create. |
path |
string | one of path/branch | Absolute or relative worktree path. |
branch |
string | one of path/branch | Matched against git worktree list. |
focus |
bool | no | Default false. |
Idempotent: if the worktree is already open as a workspace, returns that workspace_id instead
of creating a duplicate. Result shape matches worktree.create. Errors: invalid_params,
worktree_not_found.
| Field | Type | Required | Notes |
|---|---|---|---|
repo |
string | yes | |
path / branch |
string | one required | |
force |
bool | no | Only overrides git's dirty-worktree refusal; never passed for any other reason. Never deletes the branch. |
Result: {"removed": true, "closed_workspace_id"?}. Closes the associated workspace if it was
open. Errors: invalid_params, worktree_not_found, worktree_dirty, git_command_failed.
{repo?} -> {"repo", "worktrees": [{"path", "branch", "head", "is_open", "workspace_id"?, "workspace_ref"?}]}.
Errors: not_a_git_repo.
{name, force?} -> {"name", "path"}. Captures the current workspace via
Workspace.captureCustomLayout() (pane/split tree, cwds, browser URLs — not
command/env/focus, which have no live "what's running" signal). Errors: already_exists
(unless force), invalid_name, no_active_workspace.
{name, workspace_id?, cwd?} -> {"workspace_id", "workspace_ref", "window_id"?, "window_ref"?}.
If workspace_id is omitted, creates a new (never-focused) workspace first with cwd (or its
own default cwd), then applies the layout into it — the same Workspace.applyCustomLayout
engine programa.json layouts already use, so relative cwds in the saved layout resolve
against the new workspace's root. Errors: not_found.
{} -> {"layouts": [{"name", "saved_at"}]}.
Backs the programa agent-detection CLI command (list/scaffold/test), which lets users
author their own screen-manifest for an agent programa doesn't ship a bundled manifest for.
AgentManifestLoader loads bundled manifests (Resources/AgentDetection/*.json) plus any user
override at ~/.config/programa/agent-detection/<agent-id>.json; an override fully replaces the
bundled manifest for that agent id -- no field merge. scaffold itself has no dedicated method
-- it reuses surface.read_text for the screen capture and writes the starter file client-side,
but calls agent.detection.list first when scaffolding one of the seven bundled agent ids (see
below), to seed the new file from the bundled manifest's own patterns.
{} -> {"manifests": [{"agent", "display_name", "source", "process_names", "shadows_bundled", "states"}]}.
source is "bundled" or "override". shadows_bundled is true when an override exists for
one of the seven ids in AgentManifestLoader.bundledAgentIds -- i.e. this entry silently
replaced a working bundled manifest. states is the full state-rule list (bucket, priority,
anchor_last_n_lines, patterns, confidence, source_notes), included so agent-detection scaffold --force can seed a forced override from what's currently loaded instead of writing a
blank manifest over a working one.
| Field | Type | Required | Notes |
|---|---|---|---|
workspace_id / surface_id |
string | no | Same resolution as surface.read_text; defaults to the focused surface. |
agent |
string | no | Test one manifest's state patterns directly, bypassing recognition -- the authoring shortcut right after scaffold, before recognize.screen_patterns is filled in. Omit to run the real recognition path (checks every loaded manifest's recognize.screen_patterns against the current screen, first match wins) the same way live detection would. |
Result: {"requested_agent", "recognized_via" ("explicit"|"screen_pattern"|null), "agent", "display_name", "bucket", "confidence", "matched_pattern", "workspace_id"?, "surface_id"?} (all
null when nothing was recognized/classified). Errors: not_found (unknown agent), plus
whatever surface.read_text can return for the target surface.
tests_v2/ (driven by tests_v2/programa.py) is the only test suite and the CI gate. The
v1 python suite (tests/) was deleted before the protocol itself was removed.