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

160 lines
8.3 KiB
Markdown

# 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](https://github.com/ghostty-org/ghostty/issues/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](https://github.com/ghostty-org/ghostty/issues/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 `id`s 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.