# Programa

> 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.

- Skill: `darkroomengineering/programa` (Agent Skill, multi-file: 18 files)
- Install (CLI): `npx skillmds@latest add darkroomengineering/programa`
- Raw SKILL.md: https://api.skillmd.com/api/skills/darkroomengineering/programa/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: darkroomengineering (https://skillmd.com/u/darkroomengineering)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/darkroomengineering/programa

---


<!-- Installed and managed by `programa claude install-integration` / `programa codex install-hooks` / `programa opencode install-integration`. Manual edits to an installed copy get overwritten on the next install — edit the source at repo root (darkroomengineering/programa) instead. -->

# 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:

```bash
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:

```bash
programa tree --all                       # every window, not just the current one
programa tree --workspace workspace:2     # scope to one workspace
programa --json tree                      # structured JSON (global --json flag goes before the subcommand)
```

Narrower listings, when you don't need the whole tree:

```bash
programa list-workspaces          # workspaces in the current window
programa list-panes               # panes in the current workspace
programa list-pane-surfaces       # surfaces (tabs) in the focused pane; add --pane <id> for another
programa identify                 # your own window/workspace/surface IDs as JSON
```

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:

```bash
programa new-split right                        # split the current pane
programa new-split down --workspace workspace:2  # split in a different workspace
```

Text output is `OK surface:6 workspace:2` — the new surface's handle. Send it a command without focusing it:

```bash
result=$(programa new-split right)
handle=$(echo "$result" | awk '{print $2}')   # surface:6
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):

```bash
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:

```bash
programa new-pane --direction down --workspace workspace:2
programa new-surface --pane pane:4              # new tab in an existing pane
```

## 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:

```bash
programa read-screen --surface "$handle"                          # visible viewport
programa read-screen --surface "$handle" --scrollback --lines 200 # last 200 lines of scrollback
```

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.

```bash
# 1. Split and capture the new surface's handle
result=$(programa new-split right)
handle=$(echo "$result" | awk '{print $2}')

# 2. Launch the helper agent in it
programa send --surface "$handle" "claude 'fix the failing test in foo_test.go'\n"

# 3. Check on it later without focusing it
programa read-screen --surface "$handle" --scrollback --lines 100

# 4. Answer it if it's waiting on you
programa send --surface "$handle" "yes\n"
```

Surface status through the sidebar and native notifications rather than only printing to your own pane:

```bash
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:

```bash
# Block until the build in a sibling pane finishes, up to 2 minutes
programa wait-surface --surface "$handle" --pattern 'BUILD (SUCCEEDED|FAILED)' --timeout 120

# Block until the process in a surface exits
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:

```bash
# Block until the agent in the helper pane goes idle (or has no state at all -- see below)
programa wait-surface --surface "$handle" --agent-state idle --timeout 300

# Block until it flags something it needs you for
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:

```bash
# In the helper agent's pane, once it's done:
programa wait-for -S build-complete

# In the coordinating agent:
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:

```bash
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:

```bash
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.

## Browser work

Two browsers, two jobs. Do not reach for a Chrome extension for either.

- **Local previews, smoke tests, screenshots you read back, DOM checks, console errors:** use programa's embedded browser. It opens beside your pane, keeps its own profile, and never moves the user's focus:

  ```bash
  programa browser open-split http://localhost:3000            # prints the new surface id
  programa browser --surface surface:7 snapshot --interactive  # interactive elements only
  programa browser --surface surface:7 click "button.submit" --snapshot-after
  programa browser --surface surface:7 screenshot --out /tmp/after.png
  programa browser --surface surface:7 tab close
  ```

  `programa browser --help` lists the rest (wait, fill, eval, cookies, console, errors). Network routing, viewport, and raw input injection are not supported on WKWebView and return `not_supported`. Over MCP the same calls are the `browser_*` tools of `programa-mcp`.

- **Logged-in sites, private dashboards, CI logs, anything that needs the user's real browser profile:** use Aside through its MCP server if it is registered (tools from the `aside` server, or `aside-devtools` for raw Chrome DevTools control), or delegate a whole task from the shell:

  ```bash
  aside "Open the staging dashboard and tell me whether the last deploy is green"
  ```

  `programa aside status` says whether Aside is installed and registered; `programa aside install-mcp` registers it with Claude Code and Codex. Do not run the installer yourself unless the user asks, it edits their agent config.

## 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.

