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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//! The host-owned status-line surface: the [`StatusLineDecl`] a host declares
//! through `#[plugin(statusline_fn = ...)]`, plus the runtime helpers its own
//! status-line subcommand calls.
//!
//! agentgear never synthesizes a shell script. A backend writes the declared
//! command into the harness's own single status-line slot, and the host binary
//! that command names does the rendering — so the compose step (host rows first,
//! then whatever the user already had) happens inside the host, which is what
//! [`user_original`] and [`compose`] are for.
//!
//! # Two hosts on one machine
//!
//! The slot is strictly single-value and last-writer-wins. Two agentgear hosts
//! that both declare a status line therefore stack: B installs over A and stashes
//! A's command as "the user's original", so uninstalling A and then B restores A's
//! command rather than the user's true original. Accepted and unguarded — a guard
//! would need a cross-host registry agentgear deliberately does not own.
//!
//! The spawn-depth sentinel that bounds a self-re-entering stash (see
//! [`is_own_command`]) costs this case its DEEPEST row: B runs A, A's own stash is the
//! user's true original, and that one is refused a level down. Same position as above —
//! the worst case here is a missing row.
use ;
use ;
use ;
use mpsc;
use Duration;
use Value;
use crateResult;
use crate;
/// How long the user's own status command may run before its row is dropped. The
/// harness already bounds the whole status line and ours is now nested inside that,
/// so a command of theirs that hangs must not take the host's own rows down with it.
const USER_COMMAND_TIMEOUT: Duration = from_secs;
/// Ceiling on what a status command may print. The timeout bounds wall time, not
/// bytes: a command spraying stdout would otherwise have the reader allocating flat
/// out until it fires. Far above any real status line, so this is runaway protection
/// rather than a functional limit — output past it is simply cut.
const MAX_OUTPUT_BYTES: u64 = 1024 * 1024;
/// Presence-only marker that this process is already running inside a status-line
/// render. [`run_with_timeout`] both sets it on the child it spawns — so it reaches the
/// re-entered host binary through the shell — and refuses to spawn while it is already
/// set. Only presence is read; the value carries nothing beyond being non-empty.
const NESTED_RENDER_VAR: &str = "AGENTGEAR_STATUSLINE_NESTED";
/// A host-declared status line: one shell command whose stdout is the rendered
/// bar (line-oriented — each line is one row), plus the harness's optional padding
/// knob.
///
/// Deliberately open (no `#[non_exhaustive]`): a host constructs one directly, and
/// `..Default::default()` keeps working as fields are added.
///
/// # Examples
///
/// ```
/// use agentgear::StatusLineDecl;
///
/// // `${AGENTGEAR_CLIENT}` expands to each backend's own client id on write.
/// let decl = StatusLineDecl::new("mytool statusline --client ${AGENTGEAR_CLIENT}").with_padding(0);
/// assert_eq!(decl.command, "mytool statusline --client ${AGENTGEAR_CLIENT}");
/// assert_eq!(decl.padding, Some(0));
///
/// let bare = StatusLineDecl::new("mytool statusline");
/// assert_eq!(bare.padding, None);
/// ```
/// The status line this machine's user had before `client`'s backend wrote the
/// host's own, or `None` when the slot was empty (or nothing is installed).
///
/// Scope resolution is project-then-user: with `cwd` set, the marker for that
/// project scope is consulted first and the user-scope marker is the fallback, so a
/// project install shadows the user one exactly as the harness's own precedence
/// does. `client` is the backend id the host was invoked for — a plugin's
/// `${AGENTGEAR_CLIENT}` token expands to it, so a host reads it straight off its
/// own `--client` argument.
///
/// A stash naming the command the host declares RIGHT NOW reads as `None`, so the
/// common poisoned stash does not re-enter this binary from inside itself. A stash
/// naming a command the host declared in an *earlier* release is not covered here and is
/// still returned: this reader answers what the marker HOLDS, and a host may ask for
/// reasons that never spawn anything, so a depth guard here would make it lie. The
/// re-entry that opens is bounded at the spawn boundary instead, written out on this
/// module's `is_own_command`.
///
/// Ceiling on the project lookup: `cwd` is matched against the project path the
/// install was scoped to, EXACTLY. A session started in a subdirectory of that root
/// keys a different marker and silently falls back to the user-scope stash, so a
/// project-scoped status line of the user's is dropped from the bar. The harness
/// itself walks up to find its project settings; this does not. Widening it means
/// walking `cwd`'s ancestors for a marker.
///
/// # Examples
///
/// ```
/// use agentgear::{Plugin, StatusLineDecl};
///
/// // `Plugin` only comes from `PluginHost::descriptor()`, so this reader is
/// // compile-checked against the live signature without building one.
/// fn original(plugin: &Plugin) -> agentgear::Result<Option<StatusLineDecl>> {
/// agentgear::statusline::user_original(plugin, "claude", None)
/// }
/// let _ = original as fn(&Plugin) -> agentgear::Result<Option<StatusLineDecl>>;
/// ```
/// Render the full status line: the host's own `rows` first, then the rows of
/// whatever status line the user already had (run with `session_json` on its
/// stdin, exactly as the harness would have run it). Concatenation is enough
/// because the surface is line-oriented — one output line is one row.
///
/// The project-or-user scope is taken from the session JSON's `cwd` (falling back
/// to `workspace.current_dir`), then resolved by [`user_original`].
///
/// A user command that cannot be spawned, prints nothing, outlives the internal
/// timeout, or is reached from inside another render contributes nothing: a broken or
/// hung command of theirs must not blank the host's own bar.
///
/// # Examples
///
/// ```
/// use agentgear::Plugin;
///
/// fn render(plugin: &Plugin, session_json: &str) -> agentgear::Result<String> {
/// agentgear::statusline::compose(plugin, "claude", session_json, "mytool main ok")
/// }
/// let _ = render as fn(&Plugin, &str) -> agentgear::Result<String>;
/// ```
/// The scopes [`user_original`] consults, in precedence order: a project scope
/// shadows the user one, matching the harness's own whole-value override.
/// The command string a backend writes for `client`, `${AGENTGEAR_CLIENT}` expanded.
/// Whether a stashed declaration names the host's OWN command.
///
/// Running one would re-enter the very binary the harness invoked, which reads the
/// same stash and spawns again — unbounded, and re-entered on every turn the harness
/// re-renders. A stash can only carry our command through a marker written by a
/// binary whose ownership test was wrong, or by another process; either way the
/// recovery is to drop the row, never to run it.
///
/// CEILING — this compares against the command declared NOW, so a stash carrying a
/// command the host declared in an EARLIER release is still executed. Whenever that
/// rename was additive (a flag added to the same subcommand, an argument reordered) the
/// old string dispatches straight back into this binary's own status-line entrypoint,
/// which calls `compose` -> [`user_original`] -> this same stash. No wider string compare
/// closes it: a stash is verbatim harness JSON and carries no record of who wrote it, so
/// nothing derivable from the current declaration spans an arbitrary rename.
///
/// [`NESTED_RENDER_VAR`] bounds it instead, read at the spawn boundary in
/// [`run_with_timeout`]. The re-entered level renders the host's own rows and spawns
/// nothing, so depth is capped at 1: the bar carries one duplicate row instead of a
/// process chain that no level cancels (each owns a fresh [`USER_COMMAND_TIMEOUT`] and
/// [`reap`] kills only its direct child). The duplicate row is the accepted outcome.
///
/// Reachable, not theoretical: an install stamped before `Marker::statusline_command`
/// existed carries no ownership record, so the first renamed release reads its own
/// value as foreign and stashes it — producing exactly the stash this guard then fails
/// to recognise.
/// The session's working directory: Claude Code sends a top-level `cwd`, with
/// `workspace.current_dir` as the same value under the workspace block. Anything
/// unparseable resolves to user scope.
/// Run the user's own status-line command through the platform shell with
/// `session_json` on stdin, returning its trailing-newline-trimmed stdout. `None` on
/// a spawn failure, empty output, or `timeout` elapsing (the child is killed and
/// reaped). The exit code is deliberately ignored — the harness itself renders
/// whatever a status command prints.
///
/// stdin and stdout are each drained on their own thread. Writing the whole payload
/// before reading deadlocks as soon as either side outgrows its pipe buffer, and the
/// calling thread has to stay free to enforce `timeout`.
///
/// Also the spawn-depth guard, both halves: [`NESTED_RENDER_VAR`] is read on entry and
/// set on the child, so a render already nested inside another one returns `None` without
/// starting a process. Keeping the pair here means a later caller of this primitive
/// inherits the guard instead of having to remember it.
///
/// The sentinel goes on the child rather than on this process because the whole subtree
/// needs to see it (the shell hands it down to whatever it runs, host binary included),
/// and setting a process-global would need `unsafe`, which this crate forbids.
///
/// Not read in [`user_original`]: that reader is public and answers what the marker
/// holds, which a host may want for reasons that never spawn anything.
///
/// An empty value reads as absent. We only ever write `"1"`, so a blank one cannot be
/// ours — it means something else exported the name — and treating it as present would
/// silently drop the user's row on every render with nothing to observe from outside.
/// Kill (harmless once it has already exited) and reap the child so no zombie
/// outlives a host process that renders a status line every turn.