pactive 0.0.2-alpha.1 → 0.0.4-alpha.1
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/.opcteam-plugin/plugin.json +8 -4
- package/BREAKING-CHANGES.md +14 -3
- package/README.md +86 -6
- package/bin/pactive.mjs +917 -4
- package/commands/pactive.md +4 -1
- package/dsh/index.js +214 -6
- package/mcp/tools/definitions.mjs +205 -8
- package/package.json +1 -1
- package/pi/extension.ts +246 -8
- package/pi/skills/pactive/SKILL.md +57 -20
- package/skills/pactive/SKILL.md +57 -20
- package/src/core/assistants.mjs +237 -0
- package/src/core/config.mjs +40 -3
- package/src/core/fleet.mjs +80 -0
- package/src/core/guard.mjs +117 -32
- package/src/core/idle.mjs +11 -8
- package/src/core/index.mjs +3 -0
- package/src/core/scheduler.mjs +16 -4
- package/src/core/state.mjs +134 -12
- package/src/core/tick.mjs +128 -0
- package/src/hosts/herdr/fleet-view.mjs +31 -0
- package/src/hosts/herdr/index.mjs +429 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// Herdr host adapter (contract §2.1 / §2.2, I74–I79).
|
|
2
|
+
//
|
|
3
|
+
// Herdr is *not* a host we re-implement: it is the multiplexer we reuse. Nothing here creates a
|
|
4
|
+
// PTY, a tab layout or a renderer (I74) — every call is a `herdr` CLI invocation whose argv we
|
|
5
|
+
// only assemble, and every result is read back from herdr's own JSON envelope.
|
|
6
|
+
//
|
|
7
|
+
// Three rules this file exists to enforce:
|
|
8
|
+
// • I78 — herdr absent is an explicit refusal with a concrete reason, never a silent fake start.
|
|
9
|
+
// • I77 — delivery is judged on the *agent* surface (`agent prompt` + `agent get` state), never
|
|
10
|
+
// on screen text (`pane read | grep` is not evidence).
|
|
11
|
+
// • I79 — an assistant owns at most one live pane; `spawn` reuses an existing binding.
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
import { makeResolver } from '../shared/index.mjs';
|
|
18
|
+
|
|
19
|
+
export const host = 'herdr';
|
|
20
|
+
export const MODE = 'multiplexer';
|
|
21
|
+
export const MIN_VERSION = '0.9';
|
|
22
|
+
|
|
23
|
+
/** `herdr agent start --kind` values this adapter knows how to launch (§2.1). */
|
|
24
|
+
export const AGENT_KINDS = {
|
|
25
|
+
pi: 'pi',
|
|
26
|
+
claude: 'claude',
|
|
27
|
+
codex: 'codex',
|
|
28
|
+
gemini: 'gemini',
|
|
29
|
+
cursor: 'cursor',
|
|
30
|
+
opencode: 'opencode',
|
|
31
|
+
qwen: 'qwen',
|
|
32
|
+
kimi: 'kimi',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Hosts herdr cannot recognise as an agent → degraded to `pane run` + a pane-only binding (I77). */
|
|
36
|
+
export const PANE_ONLY_HOSTS = ['opcteam', 'dsh'];
|
|
37
|
+
|
|
38
|
+
/** Placeholder for the pane id that only exists after the split/tab step has run. */
|
|
39
|
+
export const PANE_TOKEN = '<pane_id>';
|
|
40
|
+
|
|
41
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
42
|
+
|
|
43
|
+
/** `PACTIVE_HERDR_BIN` → PATH `herdr`; throws HostMissingError (exit 127) like every other host. */
|
|
44
|
+
export const resolveExec = makeResolver(host, 'herdr');
|
|
45
|
+
|
|
46
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
47
|
+
/** Grace on top of herdr's own `--timeout` so herdr's verdict wins over the process kill. */
|
|
48
|
+
const PROCESS_GRACE_MS = 5000;
|
|
49
|
+
|
|
50
|
+
function timeoutFrom(env) {
|
|
51
|
+
const raw = Number(env?.PACTIVE_HERDR_TIMEOUT_MS);
|
|
52
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseEnvelope(text) {
|
|
56
|
+
const trimmed = String(text ?? '').trim();
|
|
57
|
+
if (trimmed === '') return null;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(trimmed);
|
|
60
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** I30: a child process must not inherit credential-shaped env (`*_API_KEY`, `*_TOKEN`, …). */
|
|
67
|
+
const CREDENTIAL_ENV = /(^|_)(API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?|PRIVATE_?KEY)($|_)/i;
|
|
68
|
+
|
|
69
|
+
export function childEnv(env = process.env) {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [key, value] of Object.entries(env)) {
|
|
72
|
+
if (!CREDENTIAL_ENV.test(key)) out[key] = value;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run one herdr CLI call. Success/failure comes from the exit status; the payload (or the error
|
|
79
|
+
* code herdr itself reports) is read from the JSON envelope it prints.
|
|
80
|
+
*/
|
|
81
|
+
export function run(exec, argv, ctx = {}) {
|
|
82
|
+
const env = childEnv(ctx.env ?? process.env);
|
|
83
|
+
const res = spawnSync(exec, argv, {
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
env,
|
|
86
|
+
timeout: ctx.timeoutMs ?? timeoutFrom(env),
|
|
87
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
88
|
+
});
|
|
89
|
+
const ok = res.status === 0;
|
|
90
|
+
const envelope = parseEnvelope(ok ? res.stdout : res.stderr) ?? parseEnvelope(res.stdout);
|
|
91
|
+
return {
|
|
92
|
+
ok,
|
|
93
|
+
code: res.status ?? 1,
|
|
94
|
+
argv,
|
|
95
|
+
result: envelope && envelope.result && typeof envelope.result === 'object' ? envelope.result : null,
|
|
96
|
+
error: envelope && envelope.error && typeof envelope.error === 'object' ? envelope.error : null,
|
|
97
|
+
reason: envelope?.error?.code ?? null,
|
|
98
|
+
stderr: String(res.stderr ?? '').trim(),
|
|
99
|
+
stdout: String(res.stdout ?? '').trim(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** I78: two independent preconditions, each with its own concrete reason. */
|
|
104
|
+
export function available(ctx = {}) {
|
|
105
|
+
const env = ctx.env ?? process.env;
|
|
106
|
+
const reasons = [];
|
|
107
|
+
if (env.HERDR_ENV !== '1') {
|
|
108
|
+
reasons.push({
|
|
109
|
+
code: 'HERDR_ENV_MISSING',
|
|
110
|
+
reason: `HERDR_ENV is not 1 (got ${env.HERDR_ENV === undefined ? 'unset' : JSON.stringify(env.HERDR_ENV)}) — pactive is not running inside a herdr pane`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
let exec = null;
|
|
114
|
+
try {
|
|
115
|
+
exec = resolveExec({ env });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
reasons.push({ code: err.code ?? 'HERDR_BIN_MISSING', reason: err.message });
|
|
118
|
+
}
|
|
119
|
+
const ok = reasons.length === 0;
|
|
120
|
+
return {
|
|
121
|
+
ok,
|
|
122
|
+
exec,
|
|
123
|
+
reasons,
|
|
124
|
+
reason: ok ? null : reasons.map((r) => r.reason).join('; '),
|
|
125
|
+
hint: 'daemon + outbox 仍可投递(pactive tick / pactive schedule 不依赖 herdr)',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Agent names must match `[a-z][a-z0-9_-]{0,31}` — a Chinese assistant name is not a valid agent
|
|
131
|
+
* name. The digest covers the *whole* assistantId: hashing only the `#owner` half would give every
|
|
132
|
+
* assistant of one owner the same pane name (two assistants → one agent, I79).
|
|
133
|
+
*/
|
|
134
|
+
export function agentNameFor(assistantId) {
|
|
135
|
+
const digest = crypto.createHash('sha256').update(String(assistantId ?? '')).digest('hex').slice(0, 12);
|
|
136
|
+
return `pactive-${digest}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function kindFor(hostKind) {
|
|
140
|
+
return AGENT_KINDS[hostKind] ?? null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function isPaneOnly(hostKind) {
|
|
144
|
+
return PANE_ONLY_HOSTS.includes(hostKind) || kindFor(hostKind) === null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function shellQuote(text) {
|
|
148
|
+
return /^[A-Za-z0-9_./:@%+=-]+$/.test(text) ? text : `'${String(text).replace(/'/g, `'\\''`)}'`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The degraded launch command for a host herdr has no agent kind for (§2.1 last paragraph). */
|
|
152
|
+
export function launchCommand(hostKind) {
|
|
153
|
+
return `${shellQuote(process.execPath)} ${shellQuote(path.join(PKG_ROOT, 'bin', 'pactive.mjs'))} ${hostKind}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The exact herdr argv `spawn` would run — this *is* the `--dry-run` output (zero side effects).
|
|
158
|
+
* A step marked `usesPaneId` gets PANE_TOKEN replaced with the real pane id at execution time.
|
|
159
|
+
*/
|
|
160
|
+
export function planSpawn(spec = {}) {
|
|
161
|
+
const assistantId = spec.assistantId ?? '';
|
|
162
|
+
const hostKind = spec.host ?? 'pi';
|
|
163
|
+
const cwd = spec.cwd ?? process.cwd();
|
|
164
|
+
const tab = spec.tab ?? null;
|
|
165
|
+
const direction = spec.direction === 'down' ? 'down' : 'right';
|
|
166
|
+
const agentName = agentNameFor(assistantId);
|
|
167
|
+
const kind = kindFor(hostKind);
|
|
168
|
+
const steps = [{ argv: null, usesPaneId: false }];
|
|
169
|
+
|
|
170
|
+
if (tab) {
|
|
171
|
+
steps[0].argv = ['tab', 'create', '--cwd', cwd, '--label', String(tab)];
|
|
172
|
+
} else {
|
|
173
|
+
steps[0].argv = ['pane', 'split', '--current', '--direction', direction, '--cwd', cwd, '--no-focus'];
|
|
174
|
+
}
|
|
175
|
+
steps.push({
|
|
176
|
+
argv: kind
|
|
177
|
+
? ['agent', 'start', agentName, '--kind', kind, '--pane', PANE_TOKEN]
|
|
178
|
+
: ['pane', 'run', PANE_TOKEN, launchCommand(hostKind)],
|
|
179
|
+
usesPaneId: true,
|
|
180
|
+
});
|
|
181
|
+
return { assistantId, host: hostKind, agentName, kind, surface: kind ? 'agent' : 'pane', tab, cwd, steps };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function paneIdFrom(step) {
|
|
185
|
+
const result = step.result ?? {};
|
|
186
|
+
return (
|
|
187
|
+
result.pane?.pane_id ?? result.root_pane?.pane_id ?? result.pane_id ?? null
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create the pane and start the agent/pane command. Idempotency (I79) lives one level up in
|
|
193
|
+
* `fleet up`: an assistant that already has a live binding never reaches this function.
|
|
194
|
+
*/
|
|
195
|
+
export function spawn(spec = {}, ctx = {}) {
|
|
196
|
+
const probe = available(ctx);
|
|
197
|
+
if (!probe.ok) return { ok: false, state: 'unreachable', reason: probe.reason, reasons: probe.reasons };
|
|
198
|
+
const env = ctx.env ?? process.env;
|
|
199
|
+
const plan = planSpawn(spec);
|
|
200
|
+
const commands = [];
|
|
201
|
+
|
|
202
|
+
const first = run(probe.exec, plan.steps[0].argv, { env, timeoutMs: ctx.timeoutMs });
|
|
203
|
+
commands.push({ argv: plan.steps[0].argv, ok: first.ok, code: first.code, reason: first.reason });
|
|
204
|
+
if (!first.ok) {
|
|
205
|
+
return { ok: false, state: 'error', reason: first.reason ?? first.stderr ?? 'pane-create-failed', plan, commands };
|
|
206
|
+
}
|
|
207
|
+
const paneId = paneIdFrom(first);
|
|
208
|
+
if (!paneId) {
|
|
209
|
+
return { ok: false, state: 'error', reason: 'pane-id-missing', plan, commands };
|
|
210
|
+
}
|
|
211
|
+
const tabId = first.result?.tab?.tab_id ?? first.result?.pane?.tab_id ?? null;
|
|
212
|
+
|
|
213
|
+
const argv = plan.steps[1].argv.map((part) => (part === PANE_TOKEN ? paneId : part));
|
|
214
|
+
const second = run(probe.exec, argv, { env, timeoutMs: ctx.timeoutMs });
|
|
215
|
+
commands.push({ argv, ok: second.ok, code: second.code, reason: second.reason });
|
|
216
|
+
if (!second.ok && second.reason === 'agent_name_taken') {
|
|
217
|
+
// The name is already live: a previous run left the agent running, or the state lost the
|
|
218
|
+
// binding. Reuse that pane instead of failing (I79: one assistant, one pane — never two).
|
|
219
|
+
const adopted = getAgent(plan.agentName, { env });
|
|
220
|
+
if (adopted.ok && adopted.paneId) {
|
|
221
|
+
run(probe.exec, ['pane', 'close', paneId], { env, timeoutMs: ctx.timeoutMs });
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
state: 'adopted',
|
|
225
|
+
plan,
|
|
226
|
+
commands,
|
|
227
|
+
binding: {
|
|
228
|
+
paneId: adopted.paneId,
|
|
229
|
+
agentName: plan.agentName,
|
|
230
|
+
host: plan.host,
|
|
231
|
+
surface: 'agent',
|
|
232
|
+
tabId: null,
|
|
233
|
+
boundAt: Date.now(),
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!second.ok) {
|
|
239
|
+
// The pane exists but carries no agent: close it again so a failed start leaves no orphan pane.
|
|
240
|
+
run(probe.exec, ['pane', 'close', paneId], { env, timeoutMs: ctx.timeoutMs });
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
state: 'error',
|
|
244
|
+
reason: second.reason ?? second.stderr ?? 'agent-start-failed',
|
|
245
|
+
paneId,
|
|
246
|
+
plan,
|
|
247
|
+
commands,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
state: 'bound',
|
|
254
|
+
plan,
|
|
255
|
+
commands,
|
|
256
|
+
binding: {
|
|
257
|
+
paneId,
|
|
258
|
+
agentName: plan.surface === 'agent' ? plan.agentName : null,
|
|
259
|
+
host: plan.host,
|
|
260
|
+
surface: plan.surface,
|
|
261
|
+
tabId,
|
|
262
|
+
boundAt: Date.now(),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `herdr agent list` → `[{paneId, agentName, status, cwd}]` (I76: 1:1 with `fleet ls`). */
|
|
268
|
+
export function list(ctx = {}) {
|
|
269
|
+
const probe = available(ctx);
|
|
270
|
+
if (!probe.ok) return { ok: false, agents: [], reason: probe.reason };
|
|
271
|
+
const res = run(probe.exec, ['agent', 'list'], { env: ctx.env ?? process.env });
|
|
272
|
+
if (!res.ok) return { ok: false, agents: [], reason: res.reason ?? res.stderr ?? 'agent-list-failed' };
|
|
273
|
+
const agents = Array.isArray(res.result?.agents) ? res.result.agents : [];
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
agents: agents
|
|
277
|
+
.filter((a) => a && typeof a === 'object')
|
|
278
|
+
.map((a) => ({
|
|
279
|
+
paneId: a.pane_id ?? null,
|
|
280
|
+
agentName: typeof a.name === 'string' && a.name !== '' ? a.name : null,
|
|
281
|
+
status: a.agent_status ?? 'unknown',
|
|
282
|
+
cwd: a.cwd ?? null,
|
|
283
|
+
})),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** One agent's live state (`agent get`) — the observation channel of §2.2/I77. */
|
|
288
|
+
export function getAgent(target, ctx = {}) {
|
|
289
|
+
const probe = available(ctx);
|
|
290
|
+
if (!probe.ok) return { ok: false, state: 'unreachable', reason: probe.reason };
|
|
291
|
+
const res = run(probe.exec, ['agent', 'get', target], { env: ctx.env ?? process.env });
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
const gone = res.reason === 'agent_not_found' || res.reason === 'pane_not_found';
|
|
294
|
+
return { ok: false, state: gone ? 'unreachable' : 'error', reason: res.reason ?? res.stderr ?? 'agent-get-failed' };
|
|
295
|
+
}
|
|
296
|
+
const agent = res.result?.agent ?? res.result ?? {};
|
|
297
|
+
return {
|
|
298
|
+
ok: true,
|
|
299
|
+
state: 'observed',
|
|
300
|
+
status: agent.agent_status ?? 'unknown',
|
|
301
|
+
paneId: agent.pane_id ?? null,
|
|
302
|
+
cwd: agent.cwd ?? null,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** A pane still exists? (`pane get`) */
|
|
307
|
+
export function paneAlive(paneId, ctx = {}) {
|
|
308
|
+
const probe = available(ctx);
|
|
309
|
+
if (!probe.ok) return { ok: false, alive: null, reason: probe.reason };
|
|
310
|
+
const res = run(probe.exec, ['pane', 'get', paneId], { env: ctx.env ?? process.env });
|
|
311
|
+
if (res.ok) return { ok: true, alive: true };
|
|
312
|
+
if (res.reason === 'pane_not_found') return { ok: true, alive: false, reason: 'pane_not_found' };
|
|
313
|
+
return { ok: false, alive: null, reason: res.reason ?? res.stderr ?? 'pane-get-failed' };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const UNREACHABLE_REASONS = new Set(['agent_not_found', 'pane_not_found', 'agent_exited', 'pane_closed']);
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Deliver one prompt (§2.2).
|
|
320
|
+
*
|
|
321
|
+
* `delivered` needs BOTH: the command returned ok AND `agent get` reported `working|blocked`
|
|
322
|
+
* (herdr's own states — we do not invent a vocabulary). Anything weaker is `queued` and says so;
|
|
323
|
+
* a vanished pane/agent is `unreachable` so the caller can drop the binding.
|
|
324
|
+
*/
|
|
325
|
+
export function deliver(spec = {}, ctx = {}) {
|
|
326
|
+
const env = ctx.env ?? process.env;
|
|
327
|
+
const probe = available(ctx);
|
|
328
|
+
if (!probe.ok) return { ok: false, state: 'unreachable', reason: probe.reason, at: Date.now() };
|
|
329
|
+
|
|
330
|
+
const text = String(spec.text ?? '');
|
|
331
|
+
|
|
332
|
+
// Degraded pane surface: `pane run` only acknowledges *admission*. There is no agent state to
|
|
333
|
+
// observe, so we must not call it delivered (I77) — the caller falls back to the outbox.
|
|
334
|
+
if (!spec.agentName) {
|
|
335
|
+
if (!spec.paneId) return { ok: false, state: 'unreachable', reason: 'no-agent-and-no-pane', at: Date.now() };
|
|
336
|
+
const alive = paneAlive(spec.paneId, { env });
|
|
337
|
+
if (alive.alive === false) return { ok: false, state: 'unreachable', reason: 'pane_not_found', paneId: spec.paneId, at: Date.now() };
|
|
338
|
+
const res = run(probe.exec, ['pane', 'run', spec.paneId, text], { env, timeoutMs: ctx.timeoutMs });
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
const state5 = UNREACHABLE_REASONS.has(res.reason) ? 'unreachable' : 'queued';
|
|
341
|
+
return { ok: false, state: state5, reason: res.reason ?? res.stderr ?? 'pane-run-failed', paneId: spec.paneId, at: Date.now() };
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
state: 'queued',
|
|
346
|
+
reason: 'pane-surface-unobservable',
|
|
347
|
+
paneId: spec.paneId,
|
|
348
|
+
admitted: true,
|
|
349
|
+
at: Date.now(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const before = getAgent(spec.agentName, { env });
|
|
354
|
+
if (before.state === 'unreachable') {
|
|
355
|
+
return { ok: false, state: 'unreachable', reason: before.reason, agentName: spec.agentName, at: Date.now() };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Real herdr rejects `--timeout` without `--wait` ("--timeout requires --wait"), and `--wait`
|
|
359
|
+
// *is* the activity gate we need: it returns `agent_prompt_stalled` unless herdr itself observed
|
|
360
|
+
// `working`/`blocked` within 5s (§2.2: use herdr's semantics, do not invent our own).
|
|
361
|
+
const timeoutMs = Number.isFinite(spec.timeoutMs) && spec.timeoutMs > 0 ? spec.timeoutMs : timeoutFrom(env);
|
|
362
|
+
const sent = run(
|
|
363
|
+
probe.exec,
|
|
364
|
+
['agent', 'prompt', spec.agentName, text, '--wait', '--timeout', String(timeoutMs)],
|
|
365
|
+
{ env, timeoutMs: timeoutMs + PROCESS_GRACE_MS },
|
|
366
|
+
);
|
|
367
|
+
const after = getAgent(spec.agentName, { env });
|
|
368
|
+
|
|
369
|
+
if (sent.ok) {
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
state: 'accepted',
|
|
373
|
+
reason: after.status === 'blocked' ? 'agent-blocked' : 'agent-activity-observed',
|
|
374
|
+
status: after.status ?? null,
|
|
375
|
+
agentName: spec.agentName,
|
|
376
|
+
paneId: after.paneId ?? spec.paneId ?? null,
|
|
377
|
+
at: Date.now(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// `timeout` is the ambiguous verdict (herdr: "a timeout does not prove the prompt was never
|
|
381
|
+
// delivered") — say so instead of pretending either way.
|
|
382
|
+
const ambiguous = sent.reason === 'timeout';
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
state: UNREACHABLE_REASONS.has(sent.reason) ? 'unreachable' : 'queued',
|
|
386
|
+
reason: sent.reason ?? sent.stderr ?? 'agent-prompt-failed',
|
|
387
|
+
ambiguous,
|
|
388
|
+
status: after.status ?? null,
|
|
389
|
+
agentName: spec.agentName,
|
|
390
|
+
at: Date.now(),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Close the pane (`pane close`); an already-closed pane counts as done (idempotent). */
|
|
395
|
+
export function close(paneId, ctx = {}) {
|
|
396
|
+
const probe = available(ctx);
|
|
397
|
+
if (!probe.ok) return { ok: false, state: 'unreachable', reason: probe.reason, paneId };
|
|
398
|
+
const res = run(probe.exec, ['pane', 'close', paneId], { env: ctx.env ?? process.env });
|
|
399
|
+
if (res.ok) return { ok: true, state: 'closed', paneId, at: Date.now() };
|
|
400
|
+
if (res.reason === 'pane_not_found') return { ok: true, state: 'already-closed', paneId, at: Date.now() };
|
|
401
|
+
return { ok: false, state: 'error', reason: res.reason ?? res.stderr ?? 'pane-close-failed', paneId, at: Date.now() };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Focus the assistant's pane (agent surface only; a bare pane has no focus API). */
|
|
405
|
+
export function focus(binding, ctx = {}) {
|
|
406
|
+
const probe = available(ctx);
|
|
407
|
+
if (!probe.ok) return { ok: false, state: 'unreachable', reason: probe.reason };
|
|
408
|
+
if (!binding?.agentName) {
|
|
409
|
+
return { ok: false, state: 'rejected', reason: 'pane-surface-has-no-focus-api', paneId: binding?.paneId ?? null };
|
|
410
|
+
}
|
|
411
|
+
const res = run(probe.exec, ['agent', 'focus', binding.agentName], { env: ctx.env ?? process.env });
|
|
412
|
+
if (!res.ok) return { ok: false, state: 'error', reason: res.reason ?? res.stderr ?? 'agent-focus-failed' };
|
|
413
|
+
return { ok: true, state: 'focused', paneId: binding.paneId ?? null, agentName: binding.agentName };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** The launch plan shape the other hosts expose, so `doctor`/`--print-plan` stay uniform. */
|
|
417
|
+
export function plan(ctx = {}) {
|
|
418
|
+
const probe = available(ctx);
|
|
419
|
+
return {
|
|
420
|
+
host,
|
|
421
|
+
version: MIN_VERSION,
|
|
422
|
+
mode: MODE,
|
|
423
|
+
exec: probe.exec,
|
|
424
|
+
available: probe.ok,
|
|
425
|
+
reasons: probe.reasons,
|
|
426
|
+
agentKinds: Object.keys(AGENT_KINDS),
|
|
427
|
+
paneOnlyHosts: PANE_ONLY_HOSTS,
|
|
428
|
+
};
|
|
429
|
+
}
|