repo-mailbox/docs/ghostty-orchestration-measurement.md
Kjell Tore Guttormsen 4080e22abd docs(ghostty): a ghost surface is detectable in one property read
The measurement identified realized surfaces by counting shell processes, which
is what was available while probing. morning-driver, building against this
document, found a cheaper and stronger test in the sdef: `working directory` is
readable on a terminal, and a surface with no shell process reports it EMPTY -
exactly the ghost signature the broken creation API produces.

That turns "has this surface been realized" from an inference into a
machine-checkable question, which is the guard anything driving Ghostty needs
before it types into a pane.

The same property answers one of the document's own open questions: it goes
empty when a shell exits and returns when the prompt is back, so "has this pane
returned to a prompt" can be polled rather than guessed at with a delay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
2026-08-02 15:57:27 +02:00

8.3 KiB

Driving a day plan into Ghostty: what is actually possible on 1.3.1

Measured 2026-08-01/02 against the installed terminal, not reasoned about. This document exists to answer one gate question before any design: can Ghostty be scripted to open a tab with two vertical panes, each in its own directory, with a command running in one?

Short answer: yes, but not through the documented API - the documented path is broken in the installed version, and the path that works is an undocumented composition of primitives that the maintainer has labelled preview.

The environment being measured

  • Ghostty 1.3.1 (stable, released 2026-03-13). This is the newest release; there is no 1.3.2 and no 1.4.
  • Config: working-directory = inherit, window-save-state = always, shell-integration = zsh, macos-titlebar-style = tabs.
  • Operator's workspace: one window, 13 tabs, 2 vertical panes per tab.

What was measured

Mechanism Realizes a surface? Splits? Can start a command?
AppleScript new window / new tab / split No structure only, panes blank no
ghostty +new-window not available on macOS - -
open -na Ghostty.app --args ... yes (26 shells) yes, whole layout no, and it duplicates everything
perform action on a realized surface Yes yes yes

1. The documented API is broken

new window, new tab and split return objects with the correct structure - count of tabs and count of terminals are exactly what was asked for - but no shell process is ever spawned. Nine probe windows were created across the session; the count of login ... exec -l /bin/zsh processes stayed at 29 throughout. The windows are visible and blank, and the tabs carry the placeholder ghost emoji as their name.

Any subsequent call that needs the process fails:

execution error: Ghostty got an error: Terminal surface model is not
available. (-10000)

This is issue #12730, "AppleScript: new tab via 'new tab' creates empty tab without shell process (1.3.1 regression)", closed as not planned with no maintainer comment. The independent measurement here matched the report on every detail including the ghost emoji and the full set of failed workarounds (delays up to 4s, explicit focus, explicit activate, every surface-configuration variant).

The likely cause is stated in the issue and is consistent with the 1.3.1 release notes: the fix for issue #11457 (AppleScript implicitly activating the app) appears to have removed the step that also initialized the shell.

Consequence: surface configuration is unusable. Its four useful properties

  • initial working directory, command, initial input, environment variables - all ride the broken creation path. None of them had any measurable effect: no process, no file written, no directory set.

A ghost surface can be detected in one property read, and this is the cheapest guard against the whole failure class. working directory is readable on a terminal, and a surface with no shell process reports it EMPTY - that is exactly the ghost signature. So "has this surface been realized" is a machine-checkable question, not something to infer from counting login ... exec -l /bin/zsh processes as this measurement did. Anything driving Ghostty should wait for a new surface to report a working directory before typing into it, and refuse to type into one that never does.

Contributed by morning-driver (2026-08-02) from the sdef plus its own measurement, after building against this document. The same property answers a question left open below: it goes empty when a shell process exits and returns when the prompt is back, so "has this pane finished and returned to a prompt" can be polled rather than guessed at with a delay.

2. open -na works and is still the wrong tool

open -na Ghostty.app --args --working-directory=... produced 26 live shells in the correct 13-tab / 2-pane layout. It does this by launching a second Ghostty instance (pid 80818 alongside the operator's pid 519) which then restores the saved session, because window-save-state = always.

That makes it useless for a day plan on two counts: it duplicates the entire existing workspace rather than opening a chosen subset, and session restore restores layout and working directories but never running programs - the panes came up as bare shells with no Claude Code. It also leaves the operator with two instances of their whole environment.

3. The path that works: actions on realized surfaces

The AppleScript creation commands are broken. The AppleScript action mechanism is not, and new_tab / new_split are both in ghostty +list-actions. Performing them on a surface that is already realized produces surfaces that are themselves realized:

perform action "new_tab" on <realized terminal>      -> +1 live shell
perform action "new_split:right" on <realized term>  -> +1 live shell

Measured: 29 -> 31 shells, tab count 13 -> 14, and the operator confirmed a live prompt in the new tab.

input text + send key "enter" then works on those panes. Two panes were driven to cd into different repositories and report pwd plus their own PID; both wrote distinct directories and distinct PIDs. This is the full chain, and it needs no surface configuration at all - the working directory becomes a typed cd, and the command becomes typed text.

Three constraints any implementation inherits

These came out of the measurement and are not negotiable design details.

  1. Never target a pane by position. last tab of w is not the tab that was just created; new tabs are inserted relative to the current tab, not appended. A probe that trusted position typed into a tab it did not create. The new tab must be captured by diffing tab ids before and after. This is the failure mode with the worst blast radius: the text lands in whatever program that pane is running, which may be another repo's live Claude Code session.
  2. A new tab inherits the anchor pane's working directory (working-directory = inherit), so every pane must be cd-ed explicitly. Inheriting silently produces a plan that looks right and points at the wrong repo.
  3. count of terminals is not a reliable pane count. Probe tabs reported 3 and 4 terminal objects where 2 shells existed. Panes must be addressed by the ids captured at creation, never enumerated after the fact.

Recommendation

Both paths, split across two homes - and the split is the recommendation, not a hedge.

  • The plan itself - which repos, in what order, with which startup command - belongs here, as a third rendering of the scan board.sh already performs, built with exactly the argument --brief was: it is a lookup over data the scan already holds, it costs zero model calls, and route.sh already derives the per-repo command. Printing that plan as pasteable commands is path (a), and it is complete on its own.
  • Driving Ghostty belongs in a separate repo that consumes the plan. The measurement is the argument: this is a version-pinned, undocumented composition on top of an API the maintainer calls a preview and expects to break in 1.4, whose documented path is already broken and whose regression was closed as not planned. That is a maintenance surface with its own release cadence, its own failure modes, and a blast radius that reaches into other repos' live sessions. None of that is mailbox transport, and none of it should be able to break coord-inbox or board. That board.sh lives here is not an argument for Ghostty automation living here.

The dependency runs one way: the driver consumes the plan, the plan never knows a terminal exists. If 1.4 breaks the driver, the day plan still prints and the operator still pastes.

Not measured

  • Whether input text is safe against a pane running a full-screen TUI.
  • Whether 1.3.0 is unaffected. The issue calls this a 1.3.1 regression, which implies it, but downgrading a daily driver to gain a preview API that changes in 1.4 was not evaluated as a serious option.
  • How "focus" should be expressed, how many tabs a day plan should propose, and whether it should remember yesterday. These were deferred until the gate question was answered; they are now open.