@vibest/cli 0.0.1-beta.13 → 0.0.1-beta.15
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/bin/vibest.cjs +9 -1
- package/harnesses/claude/.claude-plugin/plugin.json +4 -4
- package/harnesses/claude/README.md +48 -117
- package/harnesses/claude/hooks/hooks.json +1 -3
- package/harnesses/claude/hooks/register.mjs +289 -494
- package/harnesses/claude/skills/delegate/SKILL.md +8 -6
- package/harnesses/claude/skills/pipeline/SKILL.md +3 -3
- package/harnesses/cursor/.cursor-plugin/plugin.json +1 -1
- package/harnesses/cursor/hooks/vibest.sh +22 -1
- package/harnesses/cursor/skills/delegate/SKILL.md +8 -6
- package/harnesses/cursor/skills/pipeline/SKILL.md +3 -3
- package/harnesses/pi/extension.mjs +693 -402
- package/harnesses/pi/skills/delegate/SKILL.md +8 -6
- package/harnesses/pi/skills/pipeline/SKILL.md +3 -3
- package/package.json +25 -25
|
@@ -1,562 +1,357 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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();
|
|
151
|
-
};
|
|
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();
|
|
166
|
-
};
|
|
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");
|
|
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
|
|
187
23
|
};
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
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";
|
|
24
|
+
const put = (name, value) => {
|
|
25
|
+
if (value)
|
|
26
|
+
headers[name] = value;
|
|
212
27
|
};
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
280
|
-
var
|
|
281
|
-
var
|
|
282
|
-
var
|
|
283
|
-
var
|
|
284
|
-
var
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
88
|
+
if (!RESERVED.has(key))
|
|
334
89
|
input[key] = value;
|
|
335
90
|
}
|
|
336
91
|
return input;
|
|
337
92
|
};
|
|
338
|
-
var
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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.15"
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
421
|
-
if (
|
|
172
|
+
const tick = async (host) => {
|
|
173
|
+
if (polling)
|
|
422
174
|
return;
|
|
423
|
-
|
|
424
|
-
busy = mine;
|
|
175
|
+
polling = true;
|
|
425
176
|
try {
|
|
426
|
-
|
|
427
|
-
|
|
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)
|
|
428
194
|
return;
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
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)
|
|
201
|
+
return;
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
448
|
-
|
|
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 (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
await host.
|
|
456
|
-
|
|
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("
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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("
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
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 (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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("
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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 || event.text.trimEnd().endsWith(text.trim()))
|
|
301
|
+
return next(event);
|
|
302
|
+
return next({ ...event, context: [...event.context ?? [], text] });
|
|
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("
|
|
315
|
+
on("tool.call", async ($, event, next) => {
|
|
534
316
|
const host = nativeHost($);
|
|
535
|
-
if (!await host.
|
|
317
|
+
if (!await host.env("VIBEST_AGENT"))
|
|
536
318
|
return next(event);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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($).
|
|
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:
|
|
344
|
+
return { deny: UNREACHABLE };
|
|
553
345
|
});
|
|
554
346
|
}
|
|
555
347
|
export {
|
|
556
|
-
|
|
557
|
-
|
|
348
|
+
FULL_MAX,
|
|
349
|
+
GUARD_BACKOFF_MS,
|
|
350
|
+
GUARD_TRY_MS,
|
|
351
|
+
POLL_FAILURES_SHOWN,
|
|
558
352
|
POLL_MS,
|
|
559
|
-
|
|
560
|
-
|
|
353
|
+
REPORT_BACKOFF_MS,
|
|
354
|
+
REPORT_TRY_MS,
|
|
355
|
+
UNREACHABLE,
|
|
561
356
|
register
|
|
562
357
|
};
|