@vibest/cli 0.0.1-beta.13 → 0.0.1-beta.14

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.
@@ -1,562 +1,357 @@
1
- // src/inbox-adapter.ts
2
- var object = (stdout) => {
3
- try {
4
- const value = JSON.parse(stdout);
5
- if (typeof value !== "object" || value === null || Array.isArray(value))
6
- return;
7
- return value;
8
- } catch {
9
- return;
10
- }
11
- };
12
- var text = (value, key) => typeof value[key] === "string" ? value[key] : undefined;
13
- var flag = (value, key) => value[key] === true;
14
- var contradictory = (value, success) => text(value, "error") !== undefined && success;
15
- var adaptAttach = (code, stdout) => {
16
- if (code !== 0)
17
- return { kind: "lost" };
18
- const value = object(stdout);
19
- if (!value)
20
- return { kind: "unparsed" };
21
- const generation = text(value, "generation");
22
- const error = text(value, "error");
23
- if (generation !== undefined && generation !== "" && error === undefined && !flag(value, "empty"))
24
- return { kind: "attached", generation };
25
- if (error === "stale-receiver" && generation === undefined)
26
- return { kind: "stale-receiver" };
27
- if (error === "not-reported" && !("generation" in value))
28
- return { kind: "not-reported" };
29
- return { kind: "unparsed" };
30
- };
31
- var adaptPull = (code, stdout) => {
32
- if (code !== 0)
33
- return { kind: "lost" };
34
- const value = object(stdout);
35
- if (!value)
36
- return { kind: "unparsed" };
37
- const token = text(value, "token");
38
- const file = text(value, "file");
39
- const error = text(value, "error");
40
- const claim = token !== undefined && token !== "" && file !== undefined && file !== "";
41
- if (contradictory(value, claim || flag(value, "empty") || flag(value, "acked") || flag(value, "released")))
42
- return { kind: "unparsed" };
43
- if (claim && error === undefined && value.empty !== true)
44
- return { kind: "claim", token, file };
45
- if (value.empty === true && !claim && error === undefined)
46
- return { kind: "none" };
47
- if (error === "too-large" && !claim)
48
- return { kind: "none" };
49
- if (error === "stale-receiver" && !claim)
50
- return { kind: "stale-receiver" };
51
- return { kind: "unparsed" };
52
- };
53
- var adaptSettle = (code, stdout, op) => {
54
- if (code !== 0)
55
- return { kind: "lost" };
56
- const value = object(stdout);
57
- if (!value)
58
- return { kind: "unparsed" };
59
- const error = text(value, "error");
60
- const acked = value.acked === true;
61
- const released = value.released === true;
62
- const already = value.already === true;
63
- if (value.acked === false || value.released === false || value.already === false)
64
- return { kind: "unparsed" };
65
- if (error !== undefined && (acked || released || already))
66
- return { kind: "unparsed" };
67
- if (acked && released)
68
- return { kind: "unparsed" };
69
- const mine = op === "ack" ? acked : released;
70
- const other = op === "ack" ? released : acked;
71
- if (other)
72
- return { kind: "unparsed" };
73
- if (already && !mine)
74
- return { kind: "unparsed" };
75
- if (already && mine && error === undefined)
76
- return { kind: "already" };
77
- if (mine && error === undefined)
78
- return { kind: "ok" };
79
- if (error === "stale-receiver")
80
- return { kind: "stale-receiver" };
81
- return { kind: "unparsed" };
82
- };
83
-
84
- // src/inbox-client.ts
85
- var settlement = (phase) => {
86
- if (phase === "settle-ack")
87
- return "ack";
88
- if (phase === "settle-release-retry")
89
- return "release-retry";
90
- if (phase === "settle-release-fail")
91
- return "release-fail";
92
- return;
93
- };
94
- var createClaudeInboxClient = (io) => {
95
- let phase = "new";
96
- let generation;
97
- let boundSession;
98
- let token;
99
- let file;
100
- let stopReason;
101
- let deliveryAccepted = false;
102
- let epoch = 1;
103
- let owner = 0;
104
- let attachStarted = false;
105
- const snapshot = () => ({
106
- phase,
107
- generation,
108
- token,
109
- busy: owner !== 0,
110
- stopReason,
111
- deliveryAccepted
112
- });
113
- const stop = (reason) => {
114
- phase = "stopped";
115
- stopReason = reason;
116
- };
117
- const sessionMatches = () => boundSession !== undefined && io.sessionId() === boundSession;
118
- const foreignSession = () => {
119
- if (sessionMatches())
120
- return false;
121
- stop("session-mismatch");
122
- return true;
123
- };
124
- const live = (attempt) => attempt.epoch === epoch && attempt.generation === generation && attempt.token === token && phase !== "stopped";
125
- const start = async () => {
126
- if (attachStarted)
127
- return snapshot();
128
- attachStarted = true;
129
- const seen = epoch;
130
- const sessionId = io.sessionId();
131
- boundSession = sessionId;
132
- let reply;
133
- try {
134
- reply = await io.attach(sessionId);
135
- } catch {
136
- reply = { kind: "lost" };
137
- }
138
- if (seen !== epoch)
139
- return snapshot();
140
- if (reply.kind === "attached") {
141
- generation = reply.generation;
142
- phase = "idle";
143
- return snapshot();
144
- }
145
- if (reply.kind === "not-reported") {
146
- attachStarted = false;
147
- return snapshot();
148
- }
149
- stop(reply.kind === "stale-receiver" ? "stale-receiver" : "attach-failed");
150
- return snapshot();
1
+ // ../../protocol/src/protocol.ts
2
+ var PROTOCOL = 4;
3
+ var PROTOCOL_HEADER = "vibest-protocol";
4
+ var CALLER_HEADER = "vibest-caller";
5
+ var SESSION_HEADER = "vibest-session-id";
6
+ var REPOSITORY_HEADER = "vibest-repository";
7
+ var PROJECT_HEADER = "vibest-project-id";
8
+ var HARNESS_SESSION_HEADER = "vibest-harness-session-id";
9
+ var AGENT_HEADER = "vibest-agent";
10
+ var HARNESS_HEADER = "vibest-harness";
11
+ var CWD_HEADER = "vibest-cwd";
12
+ var RUNTIME_HEADER = "vibest-runtime";
13
+ var PLUGIN_VERSION_HEADER = "vibest-plugin-version";
14
+ var POLL_ROUTE = "/inbox/poll";
15
+ var ACK_ROUTE = "/inbox/ack";
16
+ var hookRoute = (harness, hook) => `/hooks/${harness}/${hook}`;
17
+ var isPolled = (body) => typeof body === "object" && body !== null && Array.isArray(body.ids) && typeof body.line === "string" && typeof body.text === "string";
18
+ var sessionHeaders = (who) => {
19
+ const headers = {
20
+ [PROTOCOL_HEADER]: String(PROTOCOL),
21
+ [CALLER_HEADER]: "session",
22
+ [SESSION_HEADER]: who.sessionId
151
23
  };
152
- const reattach = (nextGeneration) => {
153
- if (nextGeneration === generation)
154
- return snapshot();
155
- epoch += 1;
156
- owner = 0;
157
- attachStarted = true;
158
- generation = nextGeneration;
159
- boundSession = io.sessionId();
160
- token = undefined;
161
- file = undefined;
162
- deliveryAccepted = false;
163
- stopReason = undefined;
164
- phase = "idle";
165
- return snapshot();
24
+ const put = (name, value) => {
25
+ if (value)
26
+ headers[name] = value;
166
27
  };
167
- const settle = async (attempt) => {
168
- const op = settlement(phase);
169
- if (op === undefined || !live(attempt) || foreignSession())
170
- return;
171
- let reply;
172
- try {
173
- reply = op === "ack" ? await io.ack(attempt.generation, attempt.sessionId, attempt.token) : await io.release(attempt.generation, attempt.sessionId, attempt.token, op === "release-retry");
174
- } catch {
175
- return;
176
- }
177
- if (!live(attempt) || settlement(phase) !== op || foreignSession())
178
- return;
179
- if (reply.kind === "ok" || reply.kind === "already") {
180
- phase = "done";
181
- token = undefined;
182
- file = undefined;
183
- return;
184
- }
185
- if (reply.kind === "stale-receiver")
186
- stop("stale-receiver");
187
- };
188
- const pull = async (seen) => {
189
- if (generation === undefined || boundSession === undefined)
190
- return;
191
- const pullGeneration = generation;
192
- const pullSession = boundSession;
193
- let reply;
194
- try {
195
- reply = await io.pull(pullGeneration, pullSession);
196
- } catch {
197
- if (seen === epoch)
198
- foreignSession();
199
- return;
200
- }
201
- if (seen !== epoch || generation !== pullGeneration || foreignSession())
202
- return;
203
- if (reply.kind === "stale-receiver") {
204
- stop("stale-receiver");
205
- return;
206
- }
207
- if (reply.kind !== "claim")
208
- return;
209
- token = reply.token;
210
- file = reply.file;
211
- phase = "holding";
212
- };
213
- const consume = async (seen) => {
214
- if (token === undefined || file === undefined || generation === undefined || boundSession === undefined)
215
- return;
216
- const attempt = { epoch: seen, generation, sessionId: boundSession, token };
217
- let text;
218
- try {
219
- text = await io.readClaim(file);
220
- } catch {
221
- text = undefined;
222
- }
223
- if (!live(attempt) || foreignSession())
224
- return;
225
- if (text === undefined || text === "") {
226
- phase = "settle-release-fail";
227
- await settle(attempt);
228
- return;
229
- }
230
- phase = "submitted";
231
- let submitted;
232
- try {
233
- submitted = await io.submit(text);
234
- } catch {
235
- submitted = { kind: "ambiguous" };
236
- }
237
- if (!live(attempt) || foreignSession())
238
- return;
239
- if (submitted.kind === "accepted") {
240
- deliveryAccepted = true;
241
- phase = "settle-ack";
242
- } else if (submitted.kind === "drop") {
243
- phase = "settle-release-retry";
244
- } else {
245
- phase = "settle-release-fail";
246
- }
247
- await settle(attempt);
248
- };
249
- const tick = async () => {
250
- if (owner !== 0 || phase === "new" || phase === "stopped")
251
- return snapshot();
252
- const seen = epoch;
253
- owner = seen;
254
- try {
255
- if (!foreignSession()) {
256
- if (settlement(phase) !== undefined && generation !== undefined && token !== undefined && boundSession !== undefined) {
257
- await settle({ epoch: seen, generation, sessionId: boundSession, token });
258
- } else if (phase === "idle" || phase === "done" || phase === "holding") {
259
- if (phase === "done") {
260
- phase = "idle";
261
- deliveryAccepted = false;
262
- }
263
- if (phase === "idle")
264
- await pull(seen);
265
- if (seen === epoch && phase === "holding")
266
- await consume(seen);
267
- }
268
- }
269
- } finally {
270
- if (owner === seen)
271
- owner = 0;
272
- }
273
- return snapshot();
274
- };
275
- return { start, tick, reattach, snapshot };
28
+ put(PROJECT_HEADER, who.projectId);
29
+ put(HARNESS_SESSION_HEADER, who.harnessSessionId);
30
+ put(AGENT_HEADER, who.agent);
31
+ put(HARNESS_HEADER, who.harness);
32
+ put(CWD_HEADER, who.cwd === undefined ? undefined : encodeURIComponent(who.cwd));
33
+ put(RUNTIME_HEADER, who.runtime);
34
+ put(PLUGIN_VERSION_HEADER, who.pluginVersion);
35
+ if (who.repositoryRoot !== undefined)
36
+ headers[REPOSITORY_HEADER] = who.repositoryRoot;
37
+ return headers;
276
38
  };
277
39
 
278
40
  // src/plugin.ts
279
- var REATTACH = "vibest-reattach";
280
- var POLL_MS = 2000;
281
- var RUN_TIMEOUT_MS = 15000;
282
- var BRIDGE_TIMEOUT_MS = 1e4;
283
- var GUARD_FAILURE = "vibest integration guard failed; the tool call is refused.";
284
- var errorOf = (stdout) => {
285
- try {
286
- const value = JSON.parse(stdout);
287
- return typeof value?.error === "string" ? value.error : undefined;
288
- } catch {
289
- return;
290
- }
291
- };
292
- var refusal = (why) => {
293
- if (why === "stale-receiver")
294
- return `another receiver holds this coordinator's inbox; run /${REATTACH} to take it`;
295
- if (why === "not-reported")
296
- return `VIBEST has not recorded this Claude session yet; run /${REATTACH} once it has`;
297
- if (why === "session-mismatch")
298
- return `the Claude session changed, so delivery stopped; run /${REATTACH}`;
299
- if (why.startsWith("exit "))
300
- return `vibest inbox attach failed (${why}); check that vibest on PATH has the inbox command`;
301
- return `vibest inbox attach refused (${why}); run /${REATTACH} after fixing it`;
302
- };
303
- var unknownReplace = (why) => `vibest inbox attach --replace gave no usable answer (${why}). It may have replaced this receiver anyway, so it is not retried automatically and delivery here may stop. Run /${REATTACH} again.`;
41
+ var POLL_MS = 1000;
42
+ var POLL_FAILURES_SHOWN = 5;
43
+ var GUARD_TRY_MS = 2000;
44
+ var GUARD_BACKOFF_MS = [100, 300, 900];
45
+ var REPORT_BACKOFF_MS = [1000, 2000, 4000, 8000, 15000, 30000];
46
+ var REPORT_TRY_MS = 1e4;
47
+ var UNREACHABLE = "vibestd unreachable; the tool call is refused.";
48
+ var FULL_MAX = 100;
304
49
  function nativeHost($) {
305
50
  return {
306
- coordinator: async () => await $.env.get("VIBEST_AGENT") === "coordinator" && (await $.env.get("VIBEST_SESSION_ID") ?? "") !== "",
307
- agent: () => $.env.get("VIBEST_AGENT"),
308
- vibestSession: () => $.env.get("VIBEST_SESSION_ID"),
51
+ env: (name) => {
52
+ switch (name) {
53
+ case "VIBEST_AGENT":
54
+ return $.env.get("VIBEST_AGENT");
55
+ case "VIBEST_SESSION_ID":
56
+ return $.env.get("VIBEST_SESSION_ID");
57
+ case "VIBEST_PROJECT_ID":
58
+ return $.env.get("VIBEST_PROJECT_ID");
59
+ case "VIBEST_HARNESS":
60
+ return $.env.get("VIBEST_HARNESS");
61
+ case "VIBEST_RUNTIME":
62
+ return $.env.get("VIBEST_RUNTIME");
63
+ case "VIBEST_HOME":
64
+ return $.env.get("VIBEST_HOME");
65
+ case "HOME":
66
+ return $.env.get("HOME");
67
+ }
68
+ },
69
+ conversation: () => $.session.id(),
309
70
  cwd: () => $.session.cwd(),
310
- sessionId: () => $.session.id(),
311
- run: (argv) => $.process.run(["vibest", "inbox", ...argv], { timeoutMs: RUN_TIMEOUT_MS }),
312
- stop: (stdin) => $.process.run(["vibest", "integration", "stop", "--harness", "claude"], { stdin, timeoutMs: BRIDGE_TIMEOUT_MS }),
313
- prompt: (stdin) => $.process.run(["vibest", "integration", "prompt", "--harness", "claude"], { stdin, timeoutMs: BRIDGE_TIMEOUT_MS }),
314
- guard: (stdin) => $.process.run(["vibest", "integration", "guard", "--harness", "claude"], { stdin, timeoutMs: BRIDGE_TIMEOUT_MS }),
315
- read: (file) => $.fs.read(file),
71
+ plugin: () => $.plugin.name,
72
+ fetch: (socketPath, path, headers, body) => $.http.fetch(`http://vibestd${path}`, { method: "POST", socketPath, headers, body }),
316
73
  submit: (text) => $.prompt.submit({ text }),
317
- registerReattach: () => $.command.register({
318
- name: REATTACH,
319
- description: "Take this coordinator's VIBEST inbox with a new generation (attach --replace)",
320
- immediate: true
321
- }),
322
- every: (fn) => $.clock.every(POLL_MS, fn),
323
- status: (text) => $.ui.status(`VIBEST inbox: ${text}`),
324
- attached: () => {
325
- $.ui.status(undefined);
326
- $.ui.toast("VIBEST inbox attached");
74
+ redraw: () => $.ui.invalidate("ui.render"),
75
+ every: (ms, fn) => $.clock.every(ms, fn),
76
+ after: (ms, fn) => $.clock.after(ms, fn),
77
+ status: (text) => $.ui.status(text),
78
+ pid: async () => {
79
+ const pid = Number((await $.process.run(["sh", "-c", 'echo "$PPID"'])).stdout.trim());
80
+ return Number.isInteger(pid) && pid > 1 ? pid : undefined;
327
81
  }
328
82
  };
329
83
  }
84
+ var RESERVED = new Set(["tool", "tool_use_id", "consent", "agentId"]);
330
85
  var toolInput = (event) => {
331
86
  const input = {};
332
87
  for (const [key, value] of Object.entries(event)) {
333
- if (key !== "tool" && key !== "tool_use_id")
88
+ if (!RESERVED.has(key))
334
89
  input[key] = value;
335
90
  }
336
91
  return input;
337
92
  };
338
- var guardDenyReason = (stdout) => {
339
- let value;
93
+ var timeout = (promise, ms, after) => new Promise((resolve, reject) => {
94
+ const timer = after(ms, () => reject(new Error(`no answer in ${ms} ms`)));
95
+ promise.then((value) => {
96
+ timer.cancel();
97
+ resolve(value);
98
+ }, (error) => {
99
+ timer.cancel();
100
+ reject(error);
101
+ });
102
+ });
103
+ var parse = (text) => {
340
104
  try {
341
- value = JSON.parse(stdout);
105
+ return JSON.parse(text);
342
106
  } catch {
343
107
  return;
344
108
  }
345
- if (typeof value !== "object" || value === null || Array.isArray(value))
346
- return;
347
- const keys = Object.keys(value);
348
- if (keys.length !== 1 || keys[0] !== "hookSpecificOutput")
349
- return;
350
- const output = value.hookSpecificOutput;
351
- if (typeof output !== "object" || output === null || Array.isArray(output))
352
- return;
353
- const fields = Object.keys(output);
354
- if (fields.length !== 3 || !fields.includes("hookEventName") || !fields.includes("permissionDecision") || !fields.includes("permissionDecisionReason"))
355
- return;
356
- const denial = output;
357
- if (denial.hookEventName !== "PreToolUse" || denial.permissionDecision !== "deny" || typeof denial.permissionDecisionReason !== "string")
358
- return;
359
- return denial.permissionDecisionReason;
360
- };
361
- var guardReason = (result) => {
362
- if (result.exitCode === 0 && result.stdout.trim() === "")
363
- return;
364
- if (result.exitCode !== 0)
365
- return GUARD_FAILURE;
366
- return guardDenyReason(result.stdout) ?? GUARD_FAILURE;
367
109
  };
110
+ var errorOf = (body) => typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : undefined;
368
111
  function register(on, _options) {
369
- let started = false;
370
- let client;
371
- let lastRefusal = "unparsed";
112
+ const queue = [];
113
+ const seen = new Set;
114
+ const full = new Map;
115
+ let unacked = [];
116
+ let release = [];
117
+ let polling = false;
118
+ let draining = false;
119
+ let failures = 0;
120
+ let mismatched = false;
372
121
  let timer;
373
- let lifetime = 0;
374
- let ended = false;
375
- let current = "";
376
- let epoch = 0;
377
- let busy;
378
- let reattaching = false;
379
- const readSession = async (host) => {
380
- const seen = lifetime;
381
- const id = await host.sessionId();
382
- return seen === lifetime ? id : undefined;
122
+ const identity = async (host) => {
123
+ const agent = await host.env("VIBEST_AGENT");
124
+ const sessionId = await host.env("VIBEST_SESSION_ID");
125
+ if (!agent || !sessionId)
126
+ return;
127
+ const home = await host.env("VIBEST_HOME") || `${await host.env("HOME") ?? ""}/.vibest`;
128
+ return { agent, sessionId, socket: `${home}/vibest-http.sock` };
383
129
  };
384
- const attachArgv = (sessionId, replace) => replace ? ["attach", sessionId, "--replace"] : ["attach", sessionId];
385
- const makeClient = (host) => createClaudeInboxClient({
386
- sessionId: () => ended ? "" : current,
387
- attach: async (sessionId) => {
388
- lastRefusal = "no reply";
389
- const r = await host.run(attachArgv(sessionId, false));
390
- lastRefusal = r.exitCode !== 0 ? `exit ${r.exitCode}` : errorOf(r.stdout) ?? "unparsed";
391
- return adaptAttach(r.exitCode, r.stdout);
392
- },
393
- pull: async (generation, sessionId) => {
394
- const r = await host.run(["pull", generation, sessionId]);
395
- return adaptPull(r.exitCode, r.stdout);
396
- },
397
- readClaim: async (file) => {
398
- const body = await host.read(file);
399
- if (typeof body !== "string")
400
- throw new Error("claim body is not text");
401
- return body;
402
- },
403
- submit: async (text) => {
404
- const r = await host.submit(text);
405
- return r.drop !== undefined ? { kind: "drop" } : { kind: "accepted", text: r.text };
406
- },
407
- ack: async (generation, sessionId, token) => {
408
- const r = await host.run(["ack", generation, sessionId, token]);
409
- return adaptSettle(r.exitCode, r.stdout, "ack");
410
- },
411
- release: async (generation, sessionId, token, retry) => {
412
- const r = await host.run(["release", generation, sessionId, token, ...retry ? ["--retry"] : []]);
413
- return adaptSettle(r.exitCode, r.stdout, "release");
130
+ const post = async (host, path, body, ms) => {
131
+ const who = await identity(host);
132
+ if (!who)
133
+ throw new Error("not an VIBEST session");
134
+ const headers = sessionHeaders({
135
+ sessionId: who.sessionId,
136
+ projectId: await host.env("VIBEST_PROJECT_ID"),
137
+ harnessSessionId: await host.conversation().catch(() => {
138
+ return;
139
+ }),
140
+ agent: who.agent,
141
+ harness: await host.env("VIBEST_HARNESS") || "claude",
142
+ cwd: await host.cwd().catch(() => {
143
+ return;
144
+ }),
145
+ runtime: await host.env("VIBEST_RUNTIME"),
146
+ pluginVersion: "0.0.1-beta.14"
147
+ });
148
+ const r = await timeout(host.fetch(who.socket, path, { ...headers, "content-type": "application/json" }, JSON.stringify(body)), ms, host.after);
149
+ const reply = { status: r.status, body: parse(r.text) };
150
+ if (reply.status === 409) {
151
+ mismatched = true;
152
+ host.status(`VIBEST: ${errorOf(reply.body) ?? "vibest was updated; run vibest update"}`);
153
+ } else if (mismatched) {
154
+ mismatched = false;
155
+ host.status(undefined);
414
156
  }
415
- });
416
- const halt = () => {
417
- timer?.cancel();
418
- timer = undefined;
157
+ return reply;
158
+ };
159
+ const report = async (host, path, body) => {
160
+ const attempt = async (n) => {
161
+ let retry = true;
162
+ try {
163
+ const r = await post(host, path, body, REPORT_TRY_MS);
164
+ retry = r.status >= 500;
165
+ } catch {}
166
+ const pause = REPORT_BACKOFF_MS[n];
167
+ if (retry && pause !== undefined)
168
+ host.after(pause, () => void attempt(n + 1));
169
+ };
170
+ await attempt(0);
419
171
  };
420
- const step = async (host) => {
421
- if (!client || ended || busy === epoch)
172
+ const tick = async (host) => {
173
+ if (polling)
422
174
  return;
423
- const mine = epoch;
424
- busy = mine;
175
+ polling = true;
425
176
  try {
426
- const id = await readSession(host);
427
- if (id === undefined || ended || mine !== epoch)
177
+ if (unacked.length > 0 || release.length > 0) {
178
+ const r = await post(host, ACK_ROUTE, { ids: unacked, release }, REPORT_TRY_MS);
179
+ if (r.status === 200) {
180
+ unacked = [];
181
+ release = [];
182
+ }
183
+ }
184
+ const r = await post(host, POLL_ROUTE, {}, REPORT_TRY_MS);
185
+ if (r.status === 409)
186
+ return;
187
+ if (r.status !== 200 || !isPolled(r.body))
188
+ throw new Error(errorOf(r.body) ?? `poll answered ${r.status}`);
189
+ if (failures >= POLL_FAILURES_SHOWN)
190
+ host.status(undefined);
191
+ failures = 0;
192
+ const batch = r.body;
193
+ if (batch.ids.length === 0)
194
+ return;
195
+ const queued = (id) => queue.some((b) => b.ids.includes(id));
196
+ for (const id of batch.ids)
197
+ if (seen.has(id) && !queued(id) && !unacked.includes(id))
198
+ unacked.push(id);
199
+ const fresh = batch.ids.filter((id) => !seen.has(id));
200
+ if (fresh.length === 0)
428
201
  return;
429
- current = id;
430
- const before = client.snapshot().phase;
431
- const snap = before === "new" ? await client.start() : await client.tick();
432
- if (mine !== epoch || ended)
202
+ if (fresh.length < batch.ids.length) {
203
+ release.push(...fresh);
433
204
  return;
434
- if (before === "new" && snap.phase === "idle")
435
- host.attached();
436
- if (before === "new" && snap.phase === "new")
437
- host.status("waiting for VIBEST to record this Claude session (after a turn ends)");
438
- if (snap.phase === "stopped") {
439
- halt();
440
- host.status(refusal(snap.stopReason === "attach-failed" ? lastRefusal : snap.stopReason ?? "unparsed"));
441
205
  }
442
- } catch {} finally {
443
- if (busy === mine)
444
- busy = undefined;
206
+ for (const id of batch.ids)
207
+ seen.add(id);
208
+ queue.push(batch);
209
+ host.redraw();
210
+ } catch {
211
+ failures += 1;
212
+ if (failures === POLL_FAILURES_SHOWN)
213
+ host.status("VIBEST: vibestd does not answer; still trying");
214
+ } finally {
215
+ polling = false;
445
216
  }
446
217
  };
447
- const poll = (host) => {
448
- timer ??= host.every(() => void step(host));
218
+ const keep = (line, text) => {
219
+ const list = full.get(line);
220
+ if (list)
221
+ list.push(text);
222
+ else
223
+ full.set(line, [text]);
224
+ while (full.size > FULL_MAX)
225
+ full.delete(full.keys().next().value);
226
+ };
227
+ const forget = (line, text) => {
228
+ const list = full.get(line);
229
+ const at = list?.lastIndexOf(text) ?? -1;
230
+ if (list && at >= 0)
231
+ list.splice(at, 1);
232
+ if (list?.length === 0)
233
+ full.delete(line);
449
234
  };
450
235
  on("session.start", async ($, e, next) => {
451
236
  const host = nativeHost($);
452
- if (!started && await host.coordinator()) {
453
- started = true;
454
- client = makeClient(host);
455
- await host.registerReattach();
456
- poll(host);
237
+ if (await identity(host).catch(() => {
238
+ return;
239
+ })) {
240
+ const pid = await host.pid().catch(() => {
241
+ return;
242
+ });
243
+ report(host, "/session/ready", pid === undefined ? {} : { pid });
244
+ }
245
+ if (timer === undefined && (await identity(host))?.agent === "coordinator") {
246
+ timer = host.every(POLL_MS, () => void tick(host));
247
+ tick(host);
457
248
  }
458
249
  return next(e);
459
250
  });
460
- on("session.end", async ($, e, next) => {
461
- lifetime += 1;
462
- ended = true;
463
- if (client) {
464
- halt();
465
- nativeHost($).status(refusal("session-mismatch"));
251
+ on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
252
+ const batch = queue[0];
253
+ if (batch !== undefined && !draining) {
254
+ const host = nativeHost($);
255
+ draining = true;
256
+ queue.shift();
257
+ keep(batch.line, batch.text);
258
+ host.submit(batch.line).then((r) => {
259
+ draining = false;
260
+ if (r.drop !== undefined) {
261
+ forget(batch.line, batch.text);
262
+ queue.unshift(batch);
263
+ } else
264
+ unacked.push(...batch.ids);
265
+ if (queue.length > 0)
266
+ host.redraw();
267
+ }, () => {
268
+ draining = false;
269
+ forget(batch.line, batch.text);
270
+ queue.unshift(batch);
271
+ host.redraw();
272
+ });
466
273
  }
467
274
  return next(e);
468
275
  });
469
- on("command.run", { command: REATTACH }, async ($) => {
470
- if (!client)
471
- return { text: "Not an VIBEST coordinator session: nothing to reattach." };
472
- if (reattaching)
473
- return { text: `A /${REATTACH} is already running; wait for its answer.` };
474
- reattaching = true;
475
- const host = nativeHost($);
476
- try {
477
- const seen = lifetime;
478
- const sessionId = await readSession(host).catch(() => {
276
+ on("turn.complete", async ($, event, next) => {
277
+ if (event.agentId === undefined) {
278
+ const host = nativeHost($);
279
+ const who = await identity(host).catch(() => {
479
280
  return;
480
281
  });
481
- if (sessionId === undefined)
482
- return { text: `Could not read this Claude session; nothing was sent. Run /${REATTACH} again.` };
483
- let r;
484
- try {
485
- r = await host.run(attachArgv(sessionId, true));
486
- } catch {
487
- return { text: unknownReplace("no reply") };
488
- }
489
- const reply = adaptAttach(r.exitCode, r.stdout);
490
- if (reply.kind !== "attached") {
491
- const refused = r.exitCode === 0 ? errorOf(r.stdout) : undefined;
492
- if (refused !== undefined)
493
- return { text: `VIBEST inbox not reattached (${refused}); the inbox is unchanged.` };
494
- return { text: unknownReplace(r.exitCode !== 0 ? `exit ${r.exitCode}` : "unparsed reply") };
495
- }
496
- if (seen !== lifetime)
497
- return { text: `The Claude session changed while reattaching; generation not used here. Run /${REATTACH} again.` };
498
- current = sessionId;
499
- ended = false;
500
- epoch += 1;
501
- busy = undefined;
502
- client.reattach(reply.generation);
503
- host.attached();
504
- poll(host);
505
- return { text: "VIBEST inbox reattached with a new generation." };
506
- } finally {
507
- reattaching = false;
282
+ if (who && who.agent !== "coordinator")
283
+ report(host, hookRoute("claude", "turn-end"), {
284
+ hook_event_name: "TurnComplete",
285
+ session_id: await host.conversation().catch(() => ""),
286
+ reason: event.reason,
287
+ last_assistant_message: event.answer,
288
+ ...event.reason === "refusal" ? { refusal: event.refusal } : {}
289
+ });
508
290
  }
509
- });
510
- const reportStop = async (host, event, next) => {
511
- try {
512
- const agent = await host.agent();
513
- const session = await host.vibestSession();
514
- if (agent && session)
515
- await host.stop(JSON.stringify(event));
516
- } catch {}
517
291
  return next(event);
518
- };
519
- on("classic.Stop", ($, event, next) => reportStop(nativeHost($), event, next));
520
- on("classic.StopFailure", ($, event, next) => reportStop(nativeHost($), event, next));
521
- on("classic.UserPromptSubmit", async ($, event, next) => {
522
- try {
523
- if (!event.agent_id && (event.source === undefined || event.source === "user")) {
524
- const host = nativeHost($);
525
- const agent = await host.agent();
526
- const session = await host.vibestSession();
527
- if (agent && session && agent !== "coordinator")
528
- await host.prompt(JSON.stringify(event));
529
- }
530
- } catch {}
292
+ });
293
+ on("prompt.submit", async ($, event, next) => {
294
+ const host = nativeHost($);
295
+ if (event.origin.kind === "plugin" && event.origin.name === host.plugin()) {
296
+ const list = full.get(event.text);
297
+ const text = list?.shift();
298
+ if (list?.length === 0)
299
+ full.delete(event.text);
300
+ if (text !== undefined)
301
+ return next({ ...event, context: [...event.context ?? [], text] });
302
+ return next(event);
303
+ }
304
+ const who = await identity(host).catch(() => {
305
+ return;
306
+ });
307
+ if (who && who.agent !== "coordinator")
308
+ await report(host, hookRoute("claude", "prompt"), {
309
+ hook_event_name: "PromptSubmit",
310
+ text: event.text,
311
+ origin: event.origin.kind
312
+ });
531
313
  return next(event);
532
314
  });
533
- on("classic.PreToolUse", async ($, event, next) => {
315
+ on("tool.call", async ($, event, next) => {
534
316
  const host = nativeHost($);
535
- if (!await host.agent())
317
+ if (!await host.env("VIBEST_AGENT"))
536
318
  return next(event);
537
- try {
538
- const reason = guardReason(await host.guard(JSON.stringify({ tool_name: event.tool, tool_input: toolInput(event), cwd: await host.cwd() })));
539
- return reason === undefined ? next(event) : { deny: reason };
540
- } catch {
541
- return { deny: GUARD_FAILURE };
319
+ const body = { tool_name: event.tool, tool_input: toolInput(event), cwd: await host.cwd() };
320
+ for (let n = 0;; n++) {
321
+ try {
322
+ const r = await post(host, hookRoute("claude", "guard"), body, GUARD_TRY_MS);
323
+ if (r.status === 200) {
324
+ const deny = r.body?.deny;
325
+ return typeof deny === "string" ? { deny } : next(event);
326
+ }
327
+ if (r.status < 500)
328
+ return { deny: `vibest refused the tool call: ${errorOf(r.body) ?? `status ${r.status}`}` };
329
+ } catch {}
330
+ const pause = GUARD_BACKOFF_MS[n];
331
+ if (pause === undefined)
332
+ return { deny: UNREACHABLE };
333
+ await new Promise((resolve) => host.after(pause, resolve));
542
334
  }
543
335
  }).catch(async ($, event, next) => {
544
336
  try {
545
- if (!await nativeHost($).agent())
337
+ if (!await nativeHost($).env("VIBEST_AGENT"))
546
338
  return;
547
339
  } catch {
548
340
  return;
549
341
  }
550
342
  if (next.called)
551
343
  return next(event);
552
- return { deny: GUARD_FAILURE };
344
+ return { deny: UNREACHABLE };
553
345
  });
554
346
  }
555
347
  export {
556
- BRIDGE_TIMEOUT_MS,
557
- GUARD_FAILURE,
348
+ FULL_MAX,
349
+ GUARD_BACKOFF_MS,
350
+ GUARD_TRY_MS,
351
+ POLL_FAILURES_SHOWN,
558
352
  POLL_MS,
559
- REATTACH,
560
- RUN_TIMEOUT_MS,
353
+ REPORT_BACKOFF_MS,
354
+ REPORT_TRY_MS,
355
+ UNREACHABLE,
561
356
  register
562
357
  };