pi-legion 3.0.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +263 -2
- package/CONTRIBUTING.md +4 -17
- package/README.md +29 -22
- package/agents/worker.md +1 -1
- package/docs/contracts/display-liveness-v1.md +373 -0
- package/docs/contracts/drop-non-pi-kinds-v4.md +198 -0
- package/docs/contracts/esc-abort-v1.md +4 -4
- package/docs/contracts/esc-no-retire-v3.md +2 -2
- package/docs/contracts/launch-admission-queue-v1.md +839 -0
- package/docs/contracts/status-panel-v1.md +174 -0
- package/docs/inprocess-supervisor.md +37 -4
- package/docs/legion-v2-contract.md +18 -2
- package/docs/performance-2026-10-19.md +199 -0
- package/docs/spawn-mode-visibility-v1.md +206 -0
- package/docs/vexillatio-contract.md +3 -1
- package/index.ts +633 -84
- package/package.json +4 -2
- package/skills/pi-legion/SKILL.md +2 -3
- package/src/agents/agents.ts +17 -10
- package/src/agents/overrides.ts +0 -1
- package/src/agents/presets.ts +14 -40
- package/src/agents/settings.ts +26 -77
- package/src/agents/step-model.ts +7 -22
- package/src/api.ts +1 -35
- package/src/extension/event-bus.ts +80 -8
- package/src/extension/legion-commands.ts +43 -17
- package/src/extension/legion-doctor.ts +1 -1
- package/src/extension/legion-stop.ts +39 -13
- package/src/extension/legion-tools.ts +21 -4
- package/src/extension/liveness.ts +200 -0
- package/src/extension/notify.ts +6 -6
- package/src/extension/playbook.ts +6 -6
- package/src/extension/recycle.ts +6 -2
- package/src/extension/runtime.ts +422 -18
- package/src/extension/slash.ts +0 -21
- package/src/extension/spawn-render.ts +183 -0
- package/src/extension/summary.ts +1 -1
- package/src/legion/db.ts +165 -1
- package/src/legion/events.ts +81 -16
- package/src/legion/nodes.ts +13 -2
- package/src/legion/rebuild.ts +7 -1
- package/src/legion/usage.ts +58 -4
- package/src/runs/orchestrator.ts +1165 -62
- package/src/runs/worktree.ts +190 -21
- package/src/shared/ignored-kind.ts +57 -0
- package/src/shared/progress.ts +11 -0
- package/src/shared/session.ts +199 -19
- package/src/shared/types.ts +50 -66
- package/src/supervisor/admission-queue.ts +516 -0
- package/src/supervisor/inprocess-supervisor.ts +108 -33
- package/src/supervisor/proc-memory.ts +67 -0
- package/src/supervisor/routing-supervisor.ts +58 -1
- package/src/tui/live-overlay.ts +14 -0
- package/src/tui/session-messages-file.ts +100 -0
- package/src/tui/status.ts +103 -21
- package/src/tui/tree-view.ts +36 -3
- package/src/tui/viz/layout.ts +54 -3
- package/src/tui/viz/model.ts +67 -14
- package/src/tui/viz/palette.ts +11 -0
- package/src/tui/viz/panels.ts +222 -8
- package/src/tui/viz/railway.ts +28 -25
- package/src/tui/viz/view.ts +110 -23
- package/src/backends/legacy/client.ts +0 -638
- package/src/backends/legacy/cursor-chat.ts +0 -348
- package/src/backends/legacy/runner.ts +0 -101
- package/src/backends/legacy/types.ts +0 -138
- package/src/runs/kind.ts +0 -260
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin, testable wrapper over the `herdr` CLI.
|
|
3
|
-
*
|
|
4
|
-
* Measured behaviours this module encodes:
|
|
5
|
-
* F1 — `agent start` reports the child's session path
|
|
6
|
-
* F7 — non-pi kinds report no session ref (null)
|
|
7
|
-
* F11 — clean agent exit is `ctrl+d`, NOT `ctrl+c`
|
|
8
|
-
* F16 — `agent_name_taken` while alive; freed on exit
|
|
9
|
-
* F19 — `agent_pane_busy` race right after a pane split
|
|
10
|
-
* F21 — error JSON arrives on stderr with an empty stdout
|
|
11
|
-
* F22 — a missing binary surfaces as a start timeout, not a clear error
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { type AgentKind, ErrorCodes, DEFAULTS, SubagentError } from "../../shared/types.ts";
|
|
15
|
-
import type {
|
|
16
|
-
AgentInfo,
|
|
17
|
-
AgentStartResult,
|
|
18
|
-
CommandRunner,
|
|
19
|
-
HerdrClient,
|
|
20
|
-
HerdrError,
|
|
21
|
-
HerdrResult,
|
|
22
|
-
PaneInfo,
|
|
23
|
-
ReadSource,
|
|
24
|
-
TabInfo,
|
|
25
|
-
} from "./types.ts";
|
|
26
|
-
import { createCommandRunner, type RunnerOptions } from "./runner.ts";
|
|
27
|
-
|
|
28
|
-
interface HerdrEnvelope {
|
|
29
|
-
id?: string;
|
|
30
|
-
result?: unknown;
|
|
31
|
-
error?: { code?: string; message?: string; details?: unknown };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Map herdr's error codes onto our stable error codes. */
|
|
35
|
-
export function mapHerdrErrorCode(
|
|
36
|
-
raw: string | undefined,
|
|
37
|
-
message: string,
|
|
38
|
-
): string {
|
|
39
|
-
const code = (raw ?? "").toLowerCase();
|
|
40
|
-
const text = message.toLowerCase();
|
|
41
|
-
if (code.includes("pane_busy") || text.includes("not an available shell"))
|
|
42
|
-
return ErrorCodes.PANE_BUSY;
|
|
43
|
-
if (code.includes("name_taken") || text.includes("already used"))
|
|
44
|
-
return ErrorCodes.NAME_TAKEN;
|
|
45
|
-
if (code.includes("not_found") || text.includes("not found"))
|
|
46
|
-
return ErrorCodes.NOT_FOUND;
|
|
47
|
-
if (code.includes("timeout")) return ErrorCodes.START_TIMEOUT;
|
|
48
|
-
return raw || "HERDR_ERROR";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function firstJson(text: string): HerdrEnvelope | undefined {
|
|
52
|
-
const trimmed = text.trim();
|
|
53
|
-
if (!trimmed) return undefined;
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(trimmed) as HerdrEnvelope;
|
|
56
|
-
} catch {
|
|
57
|
-
/* fall through to line scanning */
|
|
58
|
-
}
|
|
59
|
-
// herdr may emit a non-JSON banner line before the payload.
|
|
60
|
-
const lines = trimmed.split(/\r?\n/);
|
|
61
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
62
|
-
const line = lines[i]?.trim();
|
|
63
|
-
if (!line || (!line.startsWith("{") && !line.startsWith("["))) continue;
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(line) as HerdrEnvelope;
|
|
66
|
-
} catch {
|
|
67
|
-
/* keep scanning */
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Turn a raw CLI invocation into a structured result.
|
|
75
|
-
*
|
|
76
|
-
* F21: success payloads live on stdout, error payloads on stderr.
|
|
77
|
-
* We therefore inspect BOTH, preferring the one that parses.
|
|
78
|
-
*/
|
|
79
|
-
export function parseHerdrResponse(
|
|
80
|
-
stdout: string,
|
|
81
|
-
stderr: string,
|
|
82
|
-
code: number,
|
|
83
|
-
): { ok: true; value: unknown } | { ok: false; error: HerdrError } {
|
|
84
|
-
const fromStdout = firstJson(stdout);
|
|
85
|
-
const fromStderr = firstJson(stderr);
|
|
86
|
-
|
|
87
|
-
if (fromStdout?.error) {
|
|
88
|
-
return { ok: false, error: toHerdrError(fromStdout.error) };
|
|
89
|
-
}
|
|
90
|
-
if (fromStderr?.error) {
|
|
91
|
-
return { ok: false, error: toHerdrError(fromStderr.error) };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (code === 0) {
|
|
95
|
-
if (fromStdout && "result" in fromStdout)
|
|
96
|
-
return { ok: true, value: fromStdout.result };
|
|
97
|
-
if (fromStderr && "result" in fromStderr)
|
|
98
|
-
return { ok: true, value: fromStderr.result };
|
|
99
|
-
if (fromStdout) return { ok: true, value: fromStdout };
|
|
100
|
-
if (fromStderr) return { ok: true, value: fromStderr };
|
|
101
|
-
return {
|
|
102
|
-
ok: false,
|
|
103
|
-
error: {
|
|
104
|
-
code: "PARSE_ERROR",
|
|
105
|
-
message: `no JSON payload (stdout=${stdout.slice(0, 200)})`,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Non-zero exit with no JSON error object: surface the raw text.
|
|
111
|
-
const detail = (stderr || stdout).trim().slice(0, 500);
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
error: {
|
|
115
|
-
code: code === -1 ? ErrorCodes.HERDR_UNAVAILABLE : "HERDR_ERROR",
|
|
116
|
-
message: detail || `herdr exited with code ${code}`,
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function toHerdrError(raw: {
|
|
122
|
-
code?: string;
|
|
123
|
-
message?: string;
|
|
124
|
-
details?: unknown;
|
|
125
|
-
}): HerdrError {
|
|
126
|
-
const message = raw.message ?? "unknown herdr error";
|
|
127
|
-
return {
|
|
128
|
-
code: mapHerdrErrorCode(raw.code, message),
|
|
129
|
-
message,
|
|
130
|
-
details: raw.details,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function ok<T>(value: T): HerdrResult<T> {
|
|
135
|
-
return { ok: true, value };
|
|
136
|
-
}
|
|
137
|
-
function err<T>(error: HerdrError): HerdrResult<T> {
|
|
138
|
-
return { ok: false, error };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
142
|
-
return value && typeof value === "object"
|
|
143
|
-
? (value as Record<string, unknown>)
|
|
144
|
-
: {};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function str(value: unknown): string | undefined {
|
|
148
|
-
return typeof value === "string" ? value : undefined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
// Payload normalizers
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
|
|
155
|
-
function toPaneInfo(raw: unknown): PaneInfo {
|
|
156
|
-
const r = asRecord(raw);
|
|
157
|
-
return {
|
|
158
|
-
pane_id: str(r.pane_id) ?? "",
|
|
159
|
-
tab_id: str(r.tab_id) ?? "",
|
|
160
|
-
workspace_id: str(r.workspace_id) ?? "",
|
|
161
|
-
agent_status: str(r.agent_status),
|
|
162
|
-
cwd: (str(r.cwd) ?? null) as string | null,
|
|
163
|
-
terminal_title_stripped: str(r.terminal_title_stripped),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function toAgentInfo(raw: unknown): AgentInfo {
|
|
168
|
-
const r = asRecord(raw);
|
|
169
|
-
const session = r.agent_session;
|
|
170
|
-
return {
|
|
171
|
-
name: (str(r.name) ?? null) as string | null,
|
|
172
|
-
pane_id: str(r.pane_id) ?? "",
|
|
173
|
-
tab_id: str(r.tab_id) ?? "",
|
|
174
|
-
workspace_id: str(r.workspace_id) ?? "",
|
|
175
|
-
agent: (str(r.agent) ?? null) as string | null,
|
|
176
|
-
agent_status: str(r.agent_status) ?? "unknown",
|
|
177
|
-
cwd: (str(r.cwd) ?? null) as string | null,
|
|
178
|
-
agent_session:
|
|
179
|
-
session && typeof session === "object"
|
|
180
|
-
? {
|
|
181
|
-
kind: str(asRecord(session).kind) ?? "",
|
|
182
|
-
source: str(asRecord(session).source) ?? "",
|
|
183
|
-
value: str(asRecord(session).value) ?? "",
|
|
184
|
-
}
|
|
185
|
-
: null,
|
|
186
|
-
state_labels: asRecord(r.state_labels) as Record<string, string>,
|
|
187
|
-
tokens: r.tokens,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function toTabInfo(raw: unknown): TabInfo {
|
|
192
|
-
const r = asRecord(raw);
|
|
193
|
-
return {
|
|
194
|
-
tab_id: str(r.tab_id) ?? "",
|
|
195
|
-
workspace_id: str(r.workspace_id) ?? "",
|
|
196
|
-
label: (str(r.label) ?? null) as string | null,
|
|
197
|
-
pane_count: typeof r.pane_count === "number" ? r.pane_count : 0,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* herdr wraps an agent in `{agent: {...}}` for some calls and returns it
|
|
203
|
-
* directly for others, so both shapes have to be accepted.
|
|
204
|
-
*/
|
|
205
|
-
function agentFromResult(value: unknown): AgentInfo {
|
|
206
|
-
const record = asRecord(value);
|
|
207
|
-
return toAgentInfo(record.agent ?? value);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Map `agent start`'s payload to `AgentStartResult`.
|
|
212
|
-
* The session path is the resume credential (F1), so it is only included when
|
|
213
|
-
* herdr actually reported one — a non-pi kind reports `null` (F7).
|
|
214
|
-
*/
|
|
215
|
-
function toAgentStartResult(
|
|
216
|
-
value: unknown,
|
|
217
|
-
opts: { name: string; paneId: string },
|
|
218
|
-
): AgentStartResult {
|
|
219
|
-
const record = asRecord(value);
|
|
220
|
-
const agent = toAgentInfo(record.agent);
|
|
221
|
-
// Only the pi session FILE is a `sessionPath` (F1: the resume credential).
|
|
222
|
-
// A cursor child reports `agent_session = {kind:"id", source:"herdr:cursor"}`
|
|
223
|
-
// — a chat-store id, not a path; surfacing it here would let callers treat
|
|
224
|
-
// it as a jsonl to parse.
|
|
225
|
-
const sessionPath =
|
|
226
|
-
agent.agent_session && agent.agent_session.source === "pi"
|
|
227
|
-
? agent.agent_session.value
|
|
228
|
-
: undefined;
|
|
229
|
-
return {
|
|
230
|
-
name: agent.name ?? opts.name,
|
|
231
|
-
paneId: agent.pane_id || opts.paneId,
|
|
232
|
-
argv: Array.isArray(record.argv) ? record.argv.map(String) : [],
|
|
233
|
-
...(sessionPath ? { sessionPath } : {}),
|
|
234
|
-
agentStatus: agent.agent_status,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* herdr wraps a pane in `{pane: {...}}` for some calls and returns it directly
|
|
240
|
-
* for others, so both shapes have to be accepted.
|
|
241
|
-
*/
|
|
242
|
-
function paneFromResult(value: unknown): PaneInfo {
|
|
243
|
-
const record = asRecord(value);
|
|
244
|
-
return toPaneInfo(record.pane ?? value);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Map one entry of `pane process-info`'s `foreground_processes`. */
|
|
248
|
-
// ---------------------------------------------------------------------------
|
|
249
|
-
// Client
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
|
-
|
|
252
|
-
type Call = <T>(
|
|
253
|
-
args: string[],
|
|
254
|
-
opts?: { timeoutMs?: number },
|
|
255
|
-
) => Promise<HerdrResult<T>>;
|
|
256
|
-
|
|
257
|
-
/** Append `--env K=V` for each entry. Shared by pane split and tab create. */
|
|
258
|
-
function pushEnvArgs(
|
|
259
|
-
args: string[],
|
|
260
|
-
env: Record<string, string> | undefined,
|
|
261
|
-
): void {
|
|
262
|
-
for (const [key, value] of Object.entries(env ?? {})) {
|
|
263
|
-
args.push("--env", `${key}=${value}`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* The `--cwd` / `--env` / focus trailer shared by `pane split` and `tab create`.
|
|
269
|
-
* Both accept the same three options, and keeping them in one place means the
|
|
270
|
-
* focus flag can never be forgotten on one path (it must always be explicit).
|
|
271
|
-
*/
|
|
272
|
-
function pushLaunchTrailer(
|
|
273
|
-
args: string[],
|
|
274
|
-
opts: {
|
|
275
|
-
cwd?: string;
|
|
276
|
-
env?: Record<string, string>;
|
|
277
|
-
focus?: boolean;
|
|
278
|
-
},
|
|
279
|
-
): void {
|
|
280
|
-
if (opts.cwd) args.push("--cwd", opts.cwd);
|
|
281
|
-
pushEnvArgs(args, opts.env);
|
|
282
|
-
args.push(opts.focus ? "--focus" : "--no-focus");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function paneSplitCall(
|
|
286
|
-
call: Call,
|
|
287
|
-
opts: {
|
|
288
|
-
target?: string;
|
|
289
|
-
current?: boolean;
|
|
290
|
-
direction: "right" | "down";
|
|
291
|
-
cwd?: string;
|
|
292
|
-
env?: Record<string, string>;
|
|
293
|
-
focus?: boolean;
|
|
294
|
-
},
|
|
295
|
-
): Promise<HerdrResult<PaneInfo>> {
|
|
296
|
-
return call<Record<string, unknown>>(paneSplitArgs(opts)).then((res) =>
|
|
297
|
-
res.ok ? ok(paneFromResult(res.value)) : res,
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** Build the `pane split` argv (extracted so the API factory stays flat). */
|
|
302
|
-
function paneSplitArgs(opts: {
|
|
303
|
-
target?: string;
|
|
304
|
-
current?: boolean;
|
|
305
|
-
direction: "right" | "down";
|
|
306
|
-
cwd?: string;
|
|
307
|
-
env?: Record<string, string>;
|
|
308
|
-
focus?: boolean;
|
|
309
|
-
}): string[] {
|
|
310
|
-
const args = ["pane", "split"];
|
|
311
|
-
if (opts.current) args.push("--current");
|
|
312
|
-
else if (opts.target) args.push(opts.target);
|
|
313
|
-
args.push("--direction", opts.direction);
|
|
314
|
-
pushLaunchTrailer(args, opts);
|
|
315
|
-
return args;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function paneReadCall(
|
|
319
|
-
runner: CommandRunner,
|
|
320
|
-
paneId: string,
|
|
321
|
-
opts: { source?: ReadSource; lines?: number },
|
|
322
|
-
): Promise<HerdrResult<string>> {
|
|
323
|
-
const args = ["pane", "read", paneId];
|
|
324
|
-
if (opts.source) args.push("--source", opts.source);
|
|
325
|
-
if (opts.lines) args.push("--lines", String(opts.lines));
|
|
326
|
-
// F6: `pane read` emits PLAIN TEXT, not JSON. An explicit per-call cap so
|
|
327
|
-
// the call cannot hang even under a legacy/wedged runner; the runner's own
|
|
328
|
-
// `defaultTimeoutMs` (if the caller pinned one, including 0) still wins
|
|
329
|
-
// because a shorter of the two applies — pass the default only as the
|
|
330
|
-
// per-call value, and let the runner's undefined-vs-set logic decide.
|
|
331
|
-
const { stdout, stderr, code } = await runner(args, {
|
|
332
|
-
timeoutMs: DEFAULTS.commandTimeoutMs,
|
|
333
|
-
});
|
|
334
|
-
if (code !== 0 && !stdout)
|
|
335
|
-
return err({ code: "HERDR_ERROR", message: stderr.slice(0, 500) });
|
|
336
|
-
return ok(stdout);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function paneReportMetadataArgs(opts: {
|
|
340
|
-
paneId: string;
|
|
341
|
-
source: string;
|
|
342
|
-
displayAgent?: string;
|
|
343
|
-
title?: string;
|
|
344
|
-
tokens?: Record<string, string>;
|
|
345
|
-
/** `idle=waiting 2 subagents` — labels a herdr agent_status from this source. */
|
|
346
|
-
stateLabel?: { status: string; text: string };
|
|
347
|
-
/** Auto-expiry so a crashed reporter leaves no stale label. */
|
|
348
|
-
ttlMs?: number;
|
|
349
|
-
/** Drop all state-labels set by this source. */
|
|
350
|
-
clearStateLabels?: boolean;
|
|
351
|
-
}): string[] {
|
|
352
|
-
const args = [
|
|
353
|
-
"pane",
|
|
354
|
-
"report-metadata",
|
|
355
|
-
opts.paneId,
|
|
356
|
-
"--source",
|
|
357
|
-
opts.source,
|
|
358
|
-
];
|
|
359
|
-
if (opts.displayAgent) args.push("--display-agent", opts.displayAgent);
|
|
360
|
-
if (opts.title) args.push("--title", opts.title);
|
|
361
|
-
for (const [k, v] of Object.entries(opts.tokens ?? {}))
|
|
362
|
-
args.push("--token", `${k}=${v}`);
|
|
363
|
-
if (opts.stateLabel)
|
|
364
|
-
args.push("--state-label", `${opts.stateLabel.status}=${opts.stateLabel.text}`);
|
|
365
|
-
if (opts.clearStateLabels) args.push("--clear-state-labels");
|
|
366
|
-
if (opts.ttlMs !== undefined) args.push("--ttl-ms", String(opts.ttlMs));
|
|
367
|
-
return args;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function createPaneApi(
|
|
371
|
-
call: Call,
|
|
372
|
-
runner: CommandRunner,
|
|
373
|
-
): Pick<
|
|
374
|
-
HerdrClient,
|
|
375
|
-
| "paneSplit"
|
|
376
|
-
| "paneClose"
|
|
377
|
-
| "paneRead"
|
|
378
|
-
| "paneList"
|
|
379
|
-
| "paneGet"
|
|
380
|
-
| "paneReportMetadata"
|
|
381
|
-
> {
|
|
382
|
-
return {
|
|
383
|
-
paneSplit: (opts) => paneSplitCall(call, opts),
|
|
384
|
-
|
|
385
|
-
paneClose: (paneId) => call<void>(["pane", "close", paneId]),
|
|
386
|
-
|
|
387
|
-
paneRead: (paneId, opts = {}) => paneReadCall(runner, paneId, opts),
|
|
388
|
-
|
|
389
|
-
paneList: () =>
|
|
390
|
-
call<Record<string, unknown>>(["pane", "list"]).then((res) => {
|
|
391
|
-
if (!res.ok) return res;
|
|
392
|
-
const panes = asRecord(res.value).panes;
|
|
393
|
-
return ok(Array.isArray(panes) ? panes.map(toPaneInfo) : []);
|
|
394
|
-
}),
|
|
395
|
-
|
|
396
|
-
paneGet: (paneId) =>
|
|
397
|
-
call<Record<string, unknown>>(["pane", "get", paneId]).then((res) =>
|
|
398
|
-
res.ok ? ok(paneFromResult(res.value)) : res,
|
|
399
|
-
),
|
|
400
|
-
|
|
401
|
-
paneReportMetadata: (opts) => call<void>(paneReportMetadataArgs(opts)),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function createTabApi(
|
|
406
|
-
call: Call,
|
|
407
|
-
): Pick<HerdrClient, "tabCreate" | "tabClose" | "tabList"> {
|
|
408
|
-
return {
|
|
409
|
-
async tabCreate(opts) {
|
|
410
|
-
const args = ["tab", "create"];
|
|
411
|
-
if (opts.workspaceId) args.push("--workspace", opts.workspaceId);
|
|
412
|
-
if (opts.label) args.push("--label", opts.label);
|
|
413
|
-
pushLaunchTrailer(args, opts);
|
|
414
|
-
|
|
415
|
-
const res = await call<Record<string, unknown>>(args);
|
|
416
|
-
if (!res.ok) return res;
|
|
417
|
-
const value = asRecord(res.value);
|
|
418
|
-
const rootPane = asRecord(value.root_pane);
|
|
419
|
-
return ok({
|
|
420
|
-
tab: toTabInfo(value.tab),
|
|
421
|
-
rootPaneId: str(rootPane.pane_id) ?? "",
|
|
422
|
-
});
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
tabClose(tabId) {
|
|
426
|
-
return call<void>(["tab", "close", tabId]);
|
|
427
|
-
},
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
async tabList(workspaceId) {
|
|
431
|
-
const args = ["tab", "list"];
|
|
432
|
-
if (workspaceId) args.push("--workspace", workspaceId);
|
|
433
|
-
const res = await call<Record<string, unknown>>(args);
|
|
434
|
-
if (!res.ok) return res;
|
|
435
|
-
const tabs = asRecord(res.value).tabs;
|
|
436
|
-
return ok(Array.isArray(tabs) ? tabs.map(toTabInfo) : []);
|
|
437
|
-
},
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function agentStartArgs(opts: {
|
|
442
|
-
name: string;
|
|
443
|
-
kind: AgentKind;
|
|
444
|
-
paneId: string;
|
|
445
|
-
args?: string[];
|
|
446
|
-
timeoutMs?: number;
|
|
447
|
-
}): string[] {
|
|
448
|
-
const args = [
|
|
449
|
-
"agent",
|
|
450
|
-
"start",
|
|
451
|
-
opts.name,
|
|
452
|
-
"--kind",
|
|
453
|
-
opts.kind,
|
|
454
|
-
"--pane",
|
|
455
|
-
opts.paneId,
|
|
456
|
-
];
|
|
457
|
-
if (opts.timeoutMs) args.push("--timeout", String(opts.timeoutMs));
|
|
458
|
-
if (opts.args?.length) args.push("--", ...opts.args);
|
|
459
|
-
return args;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function agentPromptArgs(
|
|
463
|
-
target: string,
|
|
464
|
-
text: string,
|
|
465
|
-
opts: { wait?: boolean; timeoutMs?: number },
|
|
466
|
-
): string[] {
|
|
467
|
-
const args = ["agent", "prompt", target, text];
|
|
468
|
-
if (opts.wait) args.push("--wait");
|
|
469
|
-
if (opts.timeoutMs) args.push("--timeout", String(opts.timeoutMs));
|
|
470
|
-
return args;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function agentWaitArgs(
|
|
474
|
-
target: string,
|
|
475
|
-
opts: { until?: string[]; timeoutMs?: number },
|
|
476
|
-
): string[] {
|
|
477
|
-
const args = ["agent", "wait", target];
|
|
478
|
-
for (const state of opts.until ?? []) args.push("--until", state);
|
|
479
|
-
if (opts.timeoutMs) args.push("--timeout", String(opts.timeoutMs));
|
|
480
|
-
return args;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/** `agent list` -> a typed array, tolerating a non-array payload. */
|
|
484
|
-
function agentListFrom(res: HerdrResult<Record<string, unknown>>) {
|
|
485
|
-
if (!res.ok) return res;
|
|
486
|
-
const agents = asRecord(res.value).agents;
|
|
487
|
-
return ok(Array.isArray(agents) ? agents.map(toAgentInfo) : []);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function createAgentApi(
|
|
491
|
-
call: Call,
|
|
492
|
-
): Pick<
|
|
493
|
-
HerdrClient,
|
|
494
|
-
| "agentStart"
|
|
495
|
-
| "agentPrompt"
|
|
496
|
-
| "agentGet"
|
|
497
|
-
| "agentList"
|
|
498
|
-
| "agentSendKeys"
|
|
499
|
-
| "agentWait"
|
|
500
|
-
> {
|
|
501
|
-
return {
|
|
502
|
-
agentStart: (opts) =>
|
|
503
|
-
call<Record<string, unknown>>(agentStartArgs(opts), {
|
|
504
|
-
timeoutMs: (opts.timeoutMs ?? 45_000) + 15_000,
|
|
505
|
-
}).then((res) =>
|
|
506
|
-
res.ok ? ok(toAgentStartResult(res.value, opts)) : res,
|
|
507
|
-
),
|
|
508
|
-
|
|
509
|
-
agentPrompt: (target, text, opts = {}) =>
|
|
510
|
-
call<Record<string, unknown>>(agentPromptArgs(target, text, opts), {
|
|
511
|
-
timeoutMs: (opts.timeoutMs ?? 0) + 20_000,
|
|
512
|
-
}).then((res) => (res.ok ? ok(agentFromResult(res.value)) : res)),
|
|
513
|
-
|
|
514
|
-
agentGet: (target) =>
|
|
515
|
-
call<Record<string, unknown>>(["agent", "get", target]).then((res) =>
|
|
516
|
-
res.ok ? ok(agentFromResult(res.value)) : res,
|
|
517
|
-
),
|
|
518
|
-
|
|
519
|
-
agentList: () =>
|
|
520
|
-
call<Record<string, unknown>>(["agent", "list"]).then(agentListFrom),
|
|
521
|
-
|
|
522
|
-
agentSendKeys: (target, ...keys) =>
|
|
523
|
-
call<void>(["agent", "send-keys", target, ...keys]),
|
|
524
|
-
|
|
525
|
-
agentWait: (target, opts = {}) =>
|
|
526
|
-
call<void>(agentWaitArgs(target, opts), {
|
|
527
|
-
timeoutMs: (opts.timeoutMs ?? 0) + 10_000,
|
|
528
|
-
}),
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function createMetaApi(
|
|
533
|
-
runner: CommandRunner,
|
|
534
|
-
): Pick<HerdrClient, "available" | "integrationStatus" | "integrationInstall"> {
|
|
535
|
-
return {
|
|
536
|
-
async available() {
|
|
537
|
-
try {
|
|
538
|
-
const res = await runner(["--version"], { timeoutMs: 5_000 });
|
|
539
|
-
return res.code === 0;
|
|
540
|
-
} catch {
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
|
|
545
|
-
async integrationStatus(target: string) {
|
|
546
|
-
// `integration status` prints human lines ("name: state (path)"),
|
|
547
|
-
// not a JSON envelope — so parseHerdrResponse's JSON path cannot be
|
|
548
|
-
// used; the raw runner output is matched directly. Unparseable or
|
|
549
|
-
// missing line → null ("cannot tell"), never an error: the caller
|
|
550
|
-
// treats null as "skip the guard" rather than fail the launch.
|
|
551
|
-
const res = await runner(["integration", "status"], {
|
|
552
|
-
timeoutMs: 5_000,
|
|
553
|
-
});
|
|
554
|
-
if (res.code !== 0) return ok(null);
|
|
555
|
-
const line = res.stdout
|
|
556
|
-
.split(/\r?\n/)
|
|
557
|
-
.find((l) => l.trimStart().startsWith(`${target}:`));
|
|
558
|
-
if (!line) return ok(null);
|
|
559
|
-
return ok(line.slice(line.indexOf(":") + 1).trim());
|
|
560
|
-
},
|
|
561
|
-
|
|
562
|
-
async integrationInstall(target: string) {
|
|
563
|
-
const res = await runner(["integration", "install", target], {
|
|
564
|
-
timeoutMs: 15_000,
|
|
565
|
-
});
|
|
566
|
-
if (res.code !== 0) {
|
|
567
|
-
return err({
|
|
568
|
-
code: "HERDR_ERROR",
|
|
569
|
-
message:
|
|
570
|
-
(res.stderr || res.stdout).trim().slice(0, 300) ||
|
|
571
|
-
`herdr integration install ${target} failed`,
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
return ok((res.stdout || res.stderr).trim());
|
|
575
|
-
},
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
export interface CreateHerdrClientOptions extends RunnerOptions {}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Create a herdr client.
|
|
583
|
-
*
|
|
584
|
-
* `options` is forwarded to `createCommandRunner`, so `bin`/`env` override as
|
|
585
|
-
* before. When the caller does not pin `defaultTimeoutMs`, the default
|
|
586
|
-
* command timeout (`DEFAULTS.commandTimeoutMs`) applies to EVERY invocation
|
|
587
|
-
* that does not carry its own timeout — the wedge-proofing described on
|
|
588
|
-
* `DEFAULTS.commandTimeoutMs`. A custom `defaultTimeoutMs` of `0` keeps the
|
|
589
|
-
* old unbounded behaviour for tests that drive a fake runner.
|
|
590
|
-
*
|
|
591
|
-
* Back-compat: passing a `CommandRunner` directly (the pre-timeout signature)
|
|
592
|
-
* is still accepted and bypasses `createCommandRunner` entirely.
|
|
593
|
-
*/
|
|
594
|
-
export function createHerdrClient(
|
|
595
|
-
options: CreateHerdrClientOptions | CommandRunner = {},
|
|
596
|
-
): HerdrClient {
|
|
597
|
-
const legacyRunner =
|
|
598
|
-
typeof options === "function" ? options : undefined;
|
|
599
|
-
const opts: CreateHerdrClientOptions =
|
|
600
|
-
typeof options === "function" ? {} : options;
|
|
601
|
-
const runner =
|
|
602
|
-
legacyRunner ??
|
|
603
|
-
createCommandRunner({
|
|
604
|
-
...opts,
|
|
605
|
-
// A caller that pins `defaultTimeoutMs` (even to 0 = unbounded) knows
|
|
606
|
-
// what it is doing; otherwise wedge-proof every short control call.
|
|
607
|
-
defaultTimeoutMs:
|
|
608
|
-
opts.defaultTimeoutMs ?? DEFAULTS.commandTimeoutMs,
|
|
609
|
-
});
|
|
610
|
-
const call: Call = async <T>(
|
|
611
|
-
args: string[],
|
|
612
|
-
opts: { timeoutMs?: number } = {},
|
|
613
|
-
): Promise<HerdrResult<T>> => {
|
|
614
|
-
const { stdout, stderr, code } = await runner(args, opts);
|
|
615
|
-
const parsed = parseHerdrResponse(stdout, stderr, code);
|
|
616
|
-
return parsed.ok ? ok(parsed.value as T) : err(parsed.error);
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
return {
|
|
620
|
-
...createPaneApi(call, runner),
|
|
621
|
-
...createTabApi(call),
|
|
622
|
-
...createAgentApi(call),
|
|
623
|
-
...createMetaApi(runner),
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/** Read a pane's recent output, preferring the unwrapped view (design §1.7). */
|
|
628
|
-
export async function readPaneDiagnostic(
|
|
629
|
-
client: HerdrClient,
|
|
630
|
-
paneId: string,
|
|
631
|
-
lines = 60,
|
|
632
|
-
source: ReadSource = "recent-unwrapped",
|
|
633
|
-
): Promise<string> {
|
|
634
|
-
const res = await client.paneRead(paneId, { source, lines });
|
|
635
|
-
return res.ok ? res.value : "";
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
export { SubagentError };
|