docs(board): measure what Ghostty 1.3.1 can actually be driven to do

The gate question before any day-plan design: can Ghostty be scripted to open
a tab with two vertical panes, each in its own directory, with a command
running in one? Measured against the installed 1.3.1 rather than reasoned
about, because this is undocumented platform behavior.

Yes, but not through the documented API. new window / new tab / split return
correctly shaped objects that never spawn a shell - nine probe windows, shell
count flat at 29 - and every call that needs the process fails with "Terminal
surface model is not available". That is issue #12730, a 1.3.1 regression
closed as not planned, and 1.3.1 is the newest release. surface configuration
is collateral: all four of its useful properties ride that path.

What does work is perform action on an ALREADY REALIZED surface. new_tab and
new_split produce live shells (29 -> 31), and input text + send key drives
them - two panes cd'd to different repos reported distinct directories and
distinct PIDs. The working directory becomes a typed cd, so the broken path is
not needed at all.

Three constraints fell out of the measurement and are recorded because each
was found by breaking something: pane targeting must diff tab ids, since
last tab of w is not the new tab and a probe that trusted position typed into
a tab it did not create; new tabs inherit the anchor's directory; and
count of terminals is not a pane count.

Recommends splitting the work - the plan here as a third rendering of the
board scan, the Ghostty driver in a separate repo. The argument is the
measurement: a preview API that breaks in 1.4, whose documented path is
already broken, with a blast radius reaching into other repos' live sessions,
does not belong behind the mailbox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GfDGWyyhnM26J4p93GSk2L
This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 06:15:47 +02:00
commit 5d9c76763d

View file

@ -0,0 +1,145 @@
# 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.
### 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.