1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
|
.Dd June 11, 2026
.Dt PTY-PROMPT 1
.Os
.Sh NAME
.Nm pty-prompt
.Nd PTY broker for driving interactive prompt-based programs
.Sh SYNOPSIS
.Nm
.Cm start
.Op Fl -session Ar name
.Op Fl -timeout Ar ms
.Op Fl -raw
.Fl -
.Ar cmd Op Ar args ...
.Nm
.Cm step
.Op Fl -session Ar name
.Op Fl -timeout Ar ms
.Op Fl -raw
.Op Ar text
.Nm
.Cm stop
.Op Fl -session Ar name
.Nm
.Cm kill-session
.Op Fl -session Ar name
.Sh DESCRIPTION
.Nm
drives interactive, prompt-based programs \(em such as
.Ic git add -p ,
.Ic git rebase -i ,
REPLs, or installer wizards \(em from a caller that cannot hold a persistent
connection, most notably a large language model (LLM) agent.
.Pp
Such programs expect a real terminal and converse in a back-and-forth of
.Em prompt \(-> answer \(-> new prompt .
An LLM tool-call, however, is a one-shot process invocation: it starts, does a
thing, and exits, leaving nothing to keep the interactive program \(em or its
pseudo-terminal \(em alive between calls.
.Pp
.Nm
solves this by splitting into two cooperating parts:
.Bl -bullet -compact
.It
a background
.Em daemon
that owns the PTY, the spawned child process, and a rolling buffer of
everything the child has written so far, and
.It
a thin
.Em CLI client
\(em the command you invoke once per request \(em which talks to the daemon over
a Unix-domain socket.
.El
.Pp
The agent therefore simply shells out to
.Ic pty-prompt step
repeatedly; the daemon holds the session open and remembers the latest output
in between.
.Ss Detecting completion
A prompt-based program never announces that it has finished and is awaiting
input. After sending input,
.Nm
waits until the child has produced
.Em no new bytes
for
.Fl -timeout
milliseconds (default 500), then returns whatever accumulated. This is a
heuristic; raise
.Fl -timeout
for slow programs.
.Pp
When the child exits, the status line (see
.Sx Status line )
reports it. The daemon stays up after the child exits so a
final
.Cm step
can still retrieve the last output;
.Cm stop
or
.Cm kill-session
then tears it down.
.Ss Output
Output is
.Em ANSI-stripped
by default: escape sequences are removed and carriage returns dropped, yielding
plain, line-oriented text. Pass
.Fl -raw
to keep escape codes verbatim, e.g. for full-screen TUIs where cursor movement
matters.
.Ss Status line
Every invocation prints a single status line to
.Em standard error
(program output stays on standard output, so the two never mix). It is always
printed, including on an empty poll while a program is still working, since that
is when the timing is most useful:
.Bd -literal -offset indent
[pty-prompt: 13:58:42 | running for 16s, quiet for 1.5s, still running]
[pty-prompt: 14:00:56 | running for 2m14s, quiet for 0.3s, exited: exit status 1]
[pty-prompt: 14:00:56 | ran for 2m14s, exited, session "build" stopped]
.Ed
.Pp
The leading
.Em HH:MM:SS
is the wall-clock time the line was printed \(em an absolute reference across
polls, since relative durations alone do not convey how much real time elapsed
between separate invocations.
.Em running for
is the time since the session started;
.Em quiet for
is the time since the child last produced output (a large, growing
.Em quiet for
distinguishes a slow-but-busy program from a hung one). The final clause is the
child's state:
.Em still running ,
.Em exited ,
or
.Em "exited: <error>" .
Errors use the same bracketed form, e.g.
.Dq Li [pty-prompt: session \(dqbuild\(dq already running for 16s (stop it first)] .
.Sh COMMANDS
.Bl -tag -width Ds
.It Cm start Oo options Oc Fl - Ar cmd Op Ar args ...
Spawn
.Ar cmd
under a PTY held by a freshly forked, detached daemon, then print the
program's initial output once it settles. Everything after
.Fl -
is the command to run. Fails if a session of the same name is already running.
.It Cm step Oo options Oc Op Ar text
The verb you iterate with. Write
.Ar text
followed by Enter, wait for output to settle, and print everything produced
since the input \(em that is, the freshly rendered next prompt (including the
echo of what you typed). With no
.Ar text ,
.Cm step
only re-polls, which is useful while a program is still working.
.It Cm wait Oo Fl -session Ar name Oc Oo Fl -max-wait Ar ms Oc Op Fl -raw
Block until the child exits, then print everything produced since the call plus
the exit status. Unlike
.Cm step ,
there is no quiescence heuristic: the only stopping condition is real
completion. Use this for non-interactive commands (e.g. a build) where you just
want the result.
.Fl -max-wait
caps the block (default 60 minutes) so a wedged child cannot hang the caller.
.It Cm stop Op Fl -session Ar name
Gracefully terminate the child (SIGTERM, then SIGKILL after a grace period) and
shut the daemon down, removing its socket and log file.
.It Cm kill-session Op Fl -session Ar name
Force-terminate the child (SIGKILL immediately) and shut the daemon down. Use
when a child is wedged and ignores SIGTERM.
.El
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl -session Ar name
Operate on the named session. Multiple independent programs may run at once.
Defaults to
.Ic default ,
so simple use needs no name.
.It Fl -timeout Ar ms
The quiescence window, in milliseconds: how long the child must stay quiet
before its output is considered settled.
Applies to
.Cm start
and
.Cm step .
Default 500.
.It Fl -max-wait Ar ms
The hard cap, in milliseconds, on how long
.Cm wait
will block for the child to exit. Default 3600000 (60 minutes).
.It Fl -raw
Return output with ANSI escape codes intact instead of stripping them. Applies
to
.Cm start ,
.Cm step
and
.Cm wait .
.El
.Sh EXAMPLES
.Ss Drive git add -p
.Bd -literal -offset indent
$ pty-prompt start -- git add -p # shows first hunk + prompt
$ pty-prompt step y # stage this hunk, show the next
$ pty-prompt step n # skip the next one
$ pty-prompt step s # split a hunk
$ pty-prompt step q # quit
$ pty-prompt stop # clean up
.Ed
.Ss Wait for a non-interactive build
.Bd -literal -offset indent
$ pty-prompt start --session build -- nix build .#thing -L
$ pty-prompt wait --session build # blocks until the build exits
$ pty-prompt stop --session build
.Ed
.Ss Two concurrent sessions
.Bd -literal -offset indent
$ pty-prompt start --session rebase -- git rebase -i HEAD~5
$ pty-prompt step --session rebase x
$ pty-prompt stop --session rebase
.Ed
.Ss Run a REPL
.Bd -literal -offset indent
$ pty-prompt start --timeout 1500 -- python3 -q
$ pty-prompt step --timeout 1000 "1 + 41"
$ pty-prompt step "exit()"
$ pty-prompt stop
.Ed
.Sh FILES
.Bl -tag -width Ds
.It Pa $XDG_RUNTIME_DIR/pty-prompt/<session>.sock
Unix-domain socket on which a session daemon listens. Falls back to a directory
under
.Ev TMPDIR
when
.Ev XDG_RUNTIME_DIR
is unset.
.It Pa $XDG_RUNTIME_DIR/pty-prompt/<session>.sock.log
Standard error of the detached daemon for that session.
.El
.Sh ENVIRONMENT
.Bl -tag -width Ds
.It Ev XDG_RUNTIME_DIR
Base directory for session sockets. When unset,
.Ev TMPDIR
(or the system temporary directory) is used instead.
.El
.Sh EXIT STATUS
.Ex -std
.Pp
A client command exits non-zero when it cannot reach the named session (for
example, no daemon is running) or when the daemon reports an error.
.Sh INSTALLATION
Build and install using Nix flakes:
.Bd -literal -offset indent
$ nix profile install git+https://codeberg.org/Profpatsch/Profpatsch?ref=canon#pty-prompt
.Ed
.Pp
This installs
.Ic pty-prompt
and this man page to your Nix profile.
.Sh IMPLEMENTATION NOTES
The daemon's lifecycle hangs off a
.Ic context.Context
cancelled by a
.Cm stop
or
.Cm kill-session
request or by
.Dv SIGTERM Ns / Ns Dv SIGINT ,
which deterministically tears down the accept loop, the PTY read loop, and the
child process. After the child exits, the daemon stays up so a final
.Cm step
can still retrieve the last output and the exit result; a subsequent
.Cm stop
then removes the session. The daemon's standard error is redirected to a
per-session log file, so a caller that pipes
.Cm start
does not block waiting for the long-lived daemon to close the inherited stream.
The output buffer is capped at the last 1 MiB to bound memory on chatty
programs.
.Sh SEE ALSO
.Xr git-add 1 ,
.Xr pty 7 ,
.Xr tmux 1
.Sh AUTHORS
.An Profpatsch
.Pp
.Nm
was created with
.An Claude Code .
.Sh CAVEATS
.Bl -bullet
.It
Completion detection is a timing heuristic; a program that pauses mid-output
for longer than
.Fl -timeout
will appear settled prematurely. Raise the timeout for slow or bursty programs.
.It
With
.Fl -raw ,
full-screen TUIs emit cursor-movement and redraw escape sequences that are
faithful but hard to read.
.It
Only the last 1 MiB of output is retained; very chatty programs lose earlier
output.
.El
|