| name | programa |
| description | Drive the programa terminal app from inside a programa surface — inspect windows/workspaces/panes/surfaces, split panes and run commands without stealing the user's focus, read output from sibling panes, spawn and coordinate a helper agent, and wait on it. Use whenever an agent is running inside programa (PROGRAMA_SURFACE_ID and PROGRAMA_SOCKET_PATH are set) and needs to control the app itself, not just the shell inside one pane. Do not use, and do not call the programa CLI at all, when those two variables are unset — that means the agent is not running inside programa. |
programa
programa is a native macOS terminal built for running many coding agents in parallel. Every terminal surface it creates is scriptable through a programa CLI that talks to a local Unix socket — split panes, read a sibling pane's output, send it keystrokes, and get notified, all without the terminal UI itself.
Guard: confirm you're actually inside programa
Check this before anything else in this skill:
if [ -z "$PROGRAMA_SURFACE_ID" ] || [ -z "$PROGRAMA_SOCKET_PATH" ]; then
echo "Not running inside programa (PROGRAMA_SURFACE_ID/PROGRAMA_SOCKET_PATH unset) — skipping programa CLI use."
fi
If either variable is unset, stop here. Don't guess a socket path, don't fall back to a default location, don't try anyway — just say you're not running inside programa and continue with normal shell commands.
Both variables are exported automatically by programa on every terminal surface it creates (no shell integration or setup required), along with PROGRAMA_WORKSPACE_ID. Every command below defaults its --workspace/--surface flags to those env vars when you omit them, so most calls need no flags at all when you're operating on your own pane.
The programa CLI is already on PATH inside a programa terminal. Verify with command -v programa.
Inspecting your surroundings
Run programa tree first — it prints the whole hierarchy (windows → workspaces → panes → surfaces) with markers for where you and the user actually are:
$ programa tree
window window:1 [current] ◀ active
└── workspace workspace:2 "api-server" [selected] ◀ active
├── pane pane:1 [focused] ◀ active
│ └── surface surface:3 [terminal] "zsh" [selected] ◀ active ◀ here
└── pane pane:4
└── surface surface:5 [terminal] "npm run dev"
◀ active — the true focused window/workspace/pane/surface path (where the user's cursor is right now)
◀ here — the surface this programa tree call was invoked from (you)
[selected] / [focused] — that level's current UI selection (not necessarily "active" — the user may be in a different window)
Useful flags:
programa tree --all
programa tree --workspace workspace:2
programa --json tree
Narrower listings, when you don't need the whole tree:
programa list-workspaces
programa list-panes
programa list-pane-surfaces
programa identify
All of the above default to your own window/workspace via the env vars when you don't pass --workspace/--window.
Splitting panes and running commands without stealing focus
programa's commands split cleanly into two groups:
- Focus-preserving — safe to call at any time from any agent:
new-split, new-pane, new-surface, send, send-key, send-panel, send-key-panel, read-screen (alias capture-pane). None of these move the user's cursor, raise the window, or change the active tab.
- Focus-changing — only call these when you actually mean to move the user's attention:
focus-pane, focus-window, focus-panel, select-workspace, next-window/previous-window/last-window.
Create a split without touching focus:
programa new-split right
programa new-split down --workspace workspace:2
Text output is OK surface:6 workspace:2 — the new surface's handle. Send it a command without focusing it:
result=$(programa new-split right)
handle=$(echo "$result" | awk '{print $2}')
programa send --surface "$handle" "npm run dev\n"
\n (or \r) sends Enter, \t sends Tab, inside the text argument to send/send-panel. Use send-key when you need a literal key event instead of typed text (ctrl+c, enter, arrow keys):
programa send-key --surface "$handle" ctrl+c
new-pane / new-surface work the same way when you want a brand-new pane or an extra tab rather than splitting the current one:
programa new-pane --direction down --workspace workspace:2
programa new-surface --pane pane:4
Reading output from a sibling pane
read-screen (alias capture-pane, for tmux muscle memory) returns terminal text as plain text — the visible viewport by default, or scrollback on request:
programa read-screen --surface "$handle"
programa read-screen --surface "$handle" --scrollback --lines 200
Use this to check a build log, a test runner, or another agent's output without switching to its pane. Treat a single read as a snapshot, not a completion signal — poll it (see "Waiting" below) if you need to know when something finishes.
Spawning a helper agent and coordinating with it
When the agent_spawn tool is available, use it for helper agents. It opens the helper as a nested workspace under the current workspace, keeps the same folder, and does not move the user's focus. Set needs_isolation only when the helper will make conflicting Git changes and genuinely needs a separate worktree. Programa then shows the helper in Agent Overview automatically.
Use a split for a long-running shell command, or as a fallback when agent_spawn is unavailable. Launch the command with send, then treat it like any other sibling pane: read its output, send follow-up input, and report through the sidebar instead of the pane the user isn't looking at.
result=$(programa new-split right)
handle=$(echo "$result" | awk '{print $2}')
programa send --surface "$handle" "claude 'fix the failing test in foo_test.go'\n"
programa read-screen --surface "$handle" --scrollback --lines 100
programa send --surface "$handle" "yes\n"
Surface status through the sidebar and native notifications rather than only printing to your own pane:
programa set-status build "compiling" --icon hammer --color "#ff9500"
programa notify --title "Helper agent done" --body "Tests pass, ready for review" --surface "$handle"
set-status writes a pill into the sidebar tab row — use a unique key per tool (build, claude_code, ...) so entries don't collide. notify fires a native notification and lights up programa's unread ring/tab indicator for that surface.
Waiting on a server, a test run, or another agent
wait-surface blocks server-side until a surface's output matches a regex or its process exits, so you don't have to poll:
programa wait-surface --surface "$handle" --pattern 'BUILD (SUCCEEDED|FAILED)' --timeout 120
programa wait-surface --surface "$handle" --exit --timeout 600
Exactly one of --pattern <regex> or --exit is required. Match on whatever the process actually prints ("PASS", "Server started", a prompt returning), not a fixed sleep duration. The wait is answered by the app the moment the condition is met — there is no missed-event window even if the output appears while the call is being issued.
wait-surface also has a third condition, --agent-state <idle|working|blocked|any_change>, for a sibling pane running another agent whose lifecycle hooks report status automatically (Claude Code/Codex/OpenCode installs wire this up for you, no extra setup) — block on what the agent is doing, not what it prints:
programa wait-surface --surface "$handle" --agent-state idle --timeout 300
programa wait-surface --surface "$handle" --agent-state blocked --timeout 300
A surface that has never reported any state counts as idle for --agent-state idle (most panes have no agent hooks installed, and "idle" almost always means "not currently busy" — which is true of a bare terminal too). blocked/working require an actual report; there's nothing to observe otherwise.
For two cooperating processes, wait-for (tmux-compatible) gives you a named rendezvous instead of scraping a log — one side signals, the other blocks until it does:
programa wait-for -S build-complete
programa wait-for build-complete --timeout 120
This is a filesystem-based signal, not a verdict on why the other side signaled — pair it with a read-screen check if you need to confirm success vs. failure.
Prompting a helper agent and waiting for it, in one call
prompt-agent combines "send a prompt" and "wait for it to finish" into a single request — for the common case of the coordinating loop in "Spawning a helper agent" above:
programa prompt-agent --surface "$handle" --timeout 300 "fix the failing test in foo_test.go"
It sends the text, waits (briefly) for the helper to report it started working, then waits for it to go idle again. If the helper never reports any activity at all, the JSON response carries a warning noting its hooks may not be installed, rather than hanging or failing outright — check that field if prompt-agent returns suspiciously fast.
Writing a recap
When the user asks for a recap or summary of a change, write it as markdown to .programa/recaps/<slug>.md (repo root resolved with git rev-parse --show-toplevel from your cwd), then open it:
programa recap open <slug>
Use the same panel that renders programa markdown open for it, so lean on its formatting:
-
```mermaid fenced blocks for flow diagrams (rendered offline, no network call)
-
GitHub-style alerts (> [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION]) for callouts
-
a :::compare block for before/after code, rendered side by side:
:::compare
```swift before
old code
```
```swift after
new code
```
:::
programa recap list shows the slugs already saved. Keep the recap itself short and plain, the same way you'd summarize the change in chat.
Reference
--workspace/--surface/--pane/--window accept either a short ref (workspace:2, surface:4) or a raw UUID; omitted, they default to $PROGRAMA_WORKSPACE_ID/$PROGRAMA_SURFACE_ID.
--json and --id-format <refs|uuids|both> are global flags and go before the subcommand: programa --json tree, programa --id-format both list-panes.
- Full command list:
programa help.
- Anything not wrapped by a dedicated subcommand is reachable directly:
programa rpc <method> [json-params] calls any socket API method.
watch-events streams a live feed of agent-state/output/workspace events over one long-lived connection — it's for dashboards and orchestrators watching many surfaces at once, not for a normal agent loop; use wait-surface/prompt-agent for "wait for one thing" instead.
- Longer walkthrough and the full socket API reference:
docs/agent-skill.md and docs/v2-api-migration.md in the programa repo.