Agent-First Terminal v0.2.0
The Windows build shipped as a window that never printed anything, because a terminal is a conversation and this one only listened — ConPTY opens by asking where the cursor is and says nothing until it is told.
The first release shipped a Windows binary that could not open a terminal. Not a slow one, not a quirky one: you ran it, a window appeared, and nothing was ever printed in it. No banner, no prompt, no echo of what you typed.
This release fixes that, and the reason it happened is more interesting than the patch.
A terminal is a conversation
It is easy to think of a terminal as a pipe with a font: bytes go in, glyphs come out. That model is enough right up until a program asks the terminal a question and waits for the answer.
Programs do this constantly. ESC[6n means where is the cursor?, and the
terminal is expected to answer with ESC[row;colR. Shells use it to measure
their own prompt; full-screen programs use it to find their footing. On Unix you
can get away with never answering, because most programs that ask have a
fallback and carry on.
Windows gives you no such slack. ConPTY opens the conversation with that
question and emits nothing at all until it is answered. The very first byte our
runtime ever received from a Windows session was ESC[6n, and because we said
nothing back, that was also the last byte. The runtime was not broken in some
subtle corner — it had simply never learned that a terminal is expected to
speak.
So it speaks now. It answers where its cursor is, as the screen stands rather
than as it opened, and reports itself healthy for ESC[5n. It declines the
private form ESC[?6n, which asks about extensions this terminal does not
implement, because a program that believes a wrong answer is worse off than one
that gets none.
Two details in the implementation are load-bearing. The replies are collected and returned rather than written from inside the parser callback, because the parser runs under a lock held by the reader thread while the writer belongs to the session manager — answering from inside would be a deadlock waiting for a busy terminal. And they are taken before secret-input mode withholds output, because a query answered late is a session that never starts, and the answer is terminal protocol rather than anything derived from what is being typed.
A default that named a path Windows cannot have
The same release had a second, plainer bug underneath the first. A session
opened without an explicit program resolved $SHELL, then /bin/sh — on every
platform. Windows has neither, so the default way to use the binary failed with
“the system cannot find the path specified”.
The fix is to ask each platform its own question: COMSPEC on Windows, SHELL
then /bin/sh elsewhere. Windows deliberately does not consult SHELL, and
that part is easy to get wrong in the other direction. It is unset for native
processes, and the one common way it is set — a session started from Git Bash
or another MSYS shell — sets it to a path inside MSYS’s own filesystem namespace,
which CreateProcessW cannot resolve. Honouring it would have broken the
default for precisely the people who had it set.
Why nobody noticed
Both defects were in a published binary, and neither was subtle once observed. They survived because of an arrangement that is easy to end up with: the continuous gate ran on Linux, and the only Windows leg lived in the release workflow — which does not start until a tag exists. Windows was exercised for the first time when the crate was one step from publication.
That is now the other half of this release. The runtime suites run on Windows on
every push, and so do the three smokes that drive the real binary end to end: the
HTTP API against a live PTY, a window delivered to a stub browser, and a nested
session inheriting delivery instead of opening a second one. What had kept them
Unix-only was scaffolding rather than the product — a readiness wait built on
selectors, which on Windows selects over sockets and not pipes; a #!/bin/sh
browser stub; a session opened on /bin/sh — and all three now come from one
shared support module, so each exists once rather than twice.
Where the platforms genuinely differ, the tests say so rather than skipping.
Windows has no signal to send a process group, so the API smoke asserts the
refusal itself — 501 signal_not_supported — and then that kill is
delivered and the session reaches exited. Skipping the section would have left
the one signal Windows does support untested.
Measure, then fix
One process note, because it changed the outcome twice.
Faced with a terminal that printed nothing, the tempting move is to guess: wrong
line ending, \r versus \r\n, some encoding subtlety. Every one of those
guesses would have been wrong, and each would have cost a full cycle to
disprove. A throwaway diagnostic that dumped the raw bytes answered it in one
run: ESC[6n, then silence.
The second time was the reverse. A test insisted an API-opened session was leaking its bearer token into the child environment — which would be a security bug. The same technique showed the scrubbing worked perfectly and the test was wrong: it typed a command into a shell that echoes what is typed, and asserted on a value an unset variable never produces. Believing that failure would have meant “fixing” something that was never broken.
The tests it left behind assert the property rather than the proxy: the bearer’s value must never reach the child, checked through a report run as the program rather than typed, with an end marker proving the whole line arrived.
Getting it
$ brew install agentfirstkit/tap/afterminal # macOS and Linux
$ scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket && scoop install afterminal # Windows
$ cargo install agent-first-terminal --locked --features api