@hasna/workflows 0.2.0 → 0.2.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.
@@ -16,1054 +16,1095 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
- // src/lanes/types.ts
20
- import { statSync } from "fs";
21
- function binaryOnPath(binary) {
22
- const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
23
- for (const entry of pathEntries) {
24
- try {
25
- const stat = statSync(`${entry}/${binary}`);
26
- if (stat.isFile())
27
- return true;
28
- } catch {}
29
- }
30
- return false;
31
- }
32
- function subscriptionEnvironment(overrides = {}) {
33
- const paid = /^(OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CODEX_API_KEY|CURSOR_API_KEY)$/;
34
- const sensitive = /(?:API[_-]?KEY|SECRET|PASSWORD|TOKEN|DATABASE_URL|CREDENTIAL)/i;
35
- const env = {};
36
- for (const [name, value] of Object.entries(process.env)) {
37
- if (value !== undefined && !paid.test(name) && (!sensitive.test(name) || name === "CLAUDE_CODE_OAUTH_TOKEN"))
38
- env[name] = value;
39
- }
40
- for (const [name, value] of Object.entries(overrides)) {
41
- if (paid.test(name) || sensitive.test(name))
42
- throw new Error(`lane environment field ${name} requires a supported scoped credential consumer`);
43
- env[name] = value;
44
- }
45
- return env;
46
- }
47
- function subscriptionSdkEnvironment(overrides = {}) {
48
- const safe = subscriptionEnvironment(overrides);
49
- return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
50
- }
51
- function withTimeout(promise, timeoutMs, what, cancel) {
52
- return new Promise((resolve, reject) => {
53
- const timer = setTimeout(() => {
54
- cancel?.();
55
- reject(new LaneTimeoutError(what, timeoutMs));
56
- }, timeoutMs);
57
- promise.then((value) => {
58
- clearTimeout(timer);
59
- resolve(value);
60
- }, (err) => {
61
- clearTimeout(timer);
62
- reject(err);
63
- });
64
- });
65
- }
66
- async function spawnCli(cmd, opts) {
67
- const started = Date.now();
68
- const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
69
- let proc;
70
- try {
71
- proc = Bun.spawn({
72
- cmd,
73
- cwd: opts.cwd,
74
- env: subscriptionEnvironment(opts.env),
75
- stdin: "ignore",
76
- stdout: "pipe",
77
- stderr: "pipe",
78
- timeout: timeoutMs
79
- });
80
- } catch (err) {
81
- const message = String(err instanceof Error ? err.message : err);
82
- if (message.includes("ENOENT") || /not found/i.test(message)) {
83
- throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
84
- }
85
- return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
86
- }
87
- const [stdout, stderr, exitCode] = await Promise.all([
88
- new Response(proc.stdout).text(),
89
- new Response(proc.stderr).text(),
90
- proc.exited
91
- ]);
92
- const output = stderr ? `${stdout}
93
- ${stderr}`.trim() : stdout.trim();
94
- return { ok: exitCode === 0, exitCode, output, durationMs: Date.now() - started };
95
- }
96
- var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000, LaneTimeoutError;
97
- var init_types = __esm(() => {
98
- LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
99
- LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
100
- constructor(message) {
101
- super(message);
102
- this.name = "LaneDependencyMissingError";
103
- }
104
- };
105
- LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
106
- constructor(message) {
107
- super(message);
108
- this.name = "LaneAdapterShapeError";
109
- }
110
- };
111
- LaneTimeoutError = class LaneTimeoutError extends Error {
112
- constructor(what, timeoutMs) {
113
- super(`${what} exceeded the ${timeoutMs}ms budget`);
114
- this.name = "LaneTimeoutError";
115
- }
116
- };
117
- });
118
-
119
- // src/lanes/claude.ts
120
- function createClaudeAdapter(deps = {}) {
121
- return {
122
- kind: "claude",
123
- async probe() {
124
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
125
- try {
126
- await loader();
127
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
128
- } catch {
129
- if (binaryOnPath(deps.cliPath ?? "claude")) {
130
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
131
- }
132
- return {
133
- kind: "claude",
134
- sdk: "@anthropic-ai/claude-agent-sdk",
135
- wired: false,
136
- reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
137
- };
138
- }
139
- },
140
- async run(job) {
141
- if (!job.prompt) {
142
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
143
- }
144
- const started = Date.now();
145
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
146
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
147
- let sdk;
148
- try {
149
- sdk = await loader();
150
- } catch (err) {
151
- return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
152
- }
153
- const query = sdk.query;
154
- if (typeof query !== "function") {
155
- throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
156
- }
157
- const prompt = job.prompt;
158
- const abortController = new AbortController;
159
- try {
160
- const { output, isError } = await withTimeout((async () => {
161
- const generator = await query({
162
- prompt,
163
- options: { model: job.model, cwd: job.cwd, maxTurns: 32, abortController, env: subscriptionSdkEnvironment(job.env) }
164
- });
165
- let output2 = "";
166
- let isError2 = false;
167
- for await (const message of generator) {
168
- if (message.type === "assistant") {
169
- const content = message.message?.content;
170
- if (Array.isArray(content)) {
171
- for (const block of content) {
172
- if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
173
- output2 += block.text;
174
- }
175
- }
176
- }
177
- }
178
- if (message.type === "error")
179
- isError2 = true;
180
- }
181
- return { output: output2, isError: isError2 };
182
- })(), timeoutMs, "claude lane", () => abortController.abort());
183
- const durationMs = Date.now() - started;
184
- return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
185
- } catch (err) {
186
- const durationMs = Date.now() - started;
187
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
188
- }
189
- }
190
- };
191
- }
192
- async function runClaudeCli(job, cliPath, started, timeoutMs) {
193
- try {
194
- const result = await spawnCli([cliPath, "-p", job.prompt], {
195
- binaryName: cliPath,
196
- packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
197
- timeoutMs,
198
- env: job.env,
199
- cwd: job.cwd
200
- });
201
- return { ...result, durationMs: Date.now() - started };
202
- } catch (err) {
203
- if (err instanceof LaneDependencyMissingError) {
204
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
19
+ // src/expr.ts
20
+ function tokenize(input) {
21
+ const tokens = [];
22
+ let i = 0;
23
+ while (i < input.length) {
24
+ const ch = input[i];
25
+ if (ch === " " || ch === "\t" || ch === `
26
+ ` || ch === "\r") {
27
+ i++;
28
+ continue;
205
29
  }
206
- throw err;
207
- }
208
- }
209
- var init_claude = __esm(() => {
210
- init_types();
211
- });
212
-
213
- // src/lanes/codex.ts
214
- function createCodexAdapter(deps = {}) {
215
- return {
216
- kind: "codex",
217
- async probe() {
218
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
219
- try {
220
- await loader();
221
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
222
- } catch {
223
- if (binaryOnPath(deps.cliPath ?? "codex")) {
224
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
225
- }
226
- return {
227
- kind: "codex",
228
- sdk: "@openai/codex-sdk",
229
- wired: false,
230
- reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
231
- };
232
- }
233
- },
234
- async run(job) {
235
- if (!job.prompt) {
236
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
237
- }
238
- const started = Date.now();
239
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
240
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
241
- let sdk;
242
- try {
243
- sdk = await loader();
244
- } catch {
245
- return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
246
- }
247
- const CodexClass = sdk.Codex;
248
- if (typeof CodexClass !== "function") {
249
- throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
250
- }
251
- const abortController = new AbortController;
252
- try {
253
- const client = await withTimeout(Promise.resolve(new CodexClass({
254
- config: { model: job.model },
255
- env: subscriptionEnvironment(job.env)
256
- })), timeoutMs, "codex lane");
257
- const thread = client.startThread?.({ model: job.model, workingDirectory: job.cwd });
258
- if (!thread || typeof thread.run !== "function") {
259
- throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
260
- }
261
- const turn = await withTimeout(thread.run(job.prompt, { signal: abortController.signal }), timeoutMs, "codex lane", () => abortController.abort());
262
- const output = turn?.finalResponse ?? "";
263
- const durationMs = Date.now() - started;
264
- return { ok: true, exitCode: 0, output, durationMs };
265
- } catch (err) {
266
- const durationMs = Date.now() - started;
267
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
268
- }
30
+ if (ch === "(") {
31
+ tokens.push({ kind: "lparen", value: "(" });
32
+ i++;
33
+ continue;
269
34
  }
270
- };
271
- }
272
- async function runCodexCli(job, cliPath, started, timeoutMs) {
273
- try {
274
- const result = await spawnCli([cliPath, "exec", "--", job.prompt], {
275
- binaryName: cliPath,
276
- packageHint: "@openai/codex-sdk or the codex CLI",
277
- timeoutMs,
278
- env: job.env,
279
- cwd: job.cwd
280
- });
281
- return { ...result, durationMs: Date.now() - started };
282
- } catch (err) {
283
- if (err instanceof LaneDependencyMissingError) {
284
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
35
+ if (ch === ")") {
36
+ tokens.push({ kind: "rparen", value: ")" });
37
+ i++;
38
+ continue;
285
39
  }
286
- throw err;
287
- }
288
- }
289
- var init_codex = __esm(() => {
290
- init_types();
291
- });
292
-
293
- // src/lanes/cursor.ts
294
- function createCursorAdapter(deps = {}) {
295
- return {
296
- kind: "cursor",
297
- async probe() {
298
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
299
- try {
300
- await loader();
301
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
302
- } catch {
303
- if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
304
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
40
+ if (ch === '"' || ch === "'") {
41
+ const quote = ch;
42
+ let j = i + 1;
43
+ let out = "";
44
+ let closed = false;
45
+ while (j < input.length) {
46
+ if (input[j] === quote) {
47
+ closed = true;
48
+ break;
305
49
  }
306
- return {
307
- kind: "cursor",
308
- sdk: "@cursor/sdk",
309
- wired: false,
310
- reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
311
- };
312
- }
313
- },
314
- async run(job) {
315
- if (!job.prompt) {
316
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
317
- }
318
- if (!deps.sdkLoader && binaryOnPath(deps.cliPath ?? "cursor-agent"))
319
- return runCursorCli(job, deps.cliPath ?? "cursor-agent", Date.now(), job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS);
320
- const started = Date.now();
321
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
322
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
323
- let sdk;
324
- try {
325
- sdk = await loader();
326
- } catch {
327
- return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
328
- }
329
- let cancel;
330
- try {
331
- subscriptionEnvironment(job.env);
332
- if (!deps.sdkLoader && process.env.CURSOR_API_KEY)
333
- throw new Error("cursor lane refuses ambient API billing; use the native subscription CLI");
334
- const run = await withTimeout(runLocalAgent(sdk, job, (callback) => {
335
- cancel = callback;
336
- }), timeoutMs, "cursor lane", () => cancel?.());
337
- const durationMs = Date.now() - started;
338
- const status = run.status;
339
- const ok = status === "finished";
340
- return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
341
- } catch (err) {
342
- if (err instanceof LaneAdapterShapeError)
343
- throw err;
344
- const durationMs = Date.now() - started;
345
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
50
+ out += input[j];
51
+ j++;
346
52
  }
53
+ if (!closed)
54
+ throw new ExprSyntaxError(`unterminated string literal at offset ${i}`);
55
+ tokens.push({ kind: "string", value: out });
56
+ i = j + 1;
57
+ continue;
347
58
  }
348
- };
59
+ const two = input.slice(i, i + 2);
60
+ if (COMPARISON_OPS.has(two)) {
61
+ tokens.push({ kind: "op", value: two });
62
+ i += 2;
63
+ continue;
64
+ }
65
+ if (ch === "<" || ch === ">" || ch === "=" || ch === "!") {
66
+ tokens.push({ kind: "op", value: ch });
67
+ i++;
68
+ continue;
69
+ }
70
+ if (/[0-9]/.test(ch)) {
71
+ let j = i;
72
+ while (j < input.length && /[0-9.]/.test(input[j]))
73
+ j++;
74
+ tokens.push({ kind: "number", value: input.slice(i, j) });
75
+ i = j;
76
+ continue;
77
+ }
78
+ if (/[A-Za-z_]/.test(ch)) {
79
+ let j = i;
80
+ while (j < input.length && /[A-Za-z0-9_.]/.test(input[j]))
81
+ j++;
82
+ tokens.push({ kind: "ident", value: input.slice(i, j) });
83
+ i = j;
84
+ continue;
85
+ }
86
+ throw new ExprSyntaxError(`unexpected character ${JSON.stringify(ch)} at offset ${i}`);
87
+ }
88
+ return tokens;
349
89
  }
350
- async function runLocalAgent(sdk, job, onCancel) {
351
- const AgentClass = sdk.Agent;
352
- const CursorClass = sdk.Cursor;
353
- if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
354
- throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
90
+
91
+ class Parser {
92
+ tokens;
93
+ pos = 0;
94
+ constructor(tokens) {
95
+ this.tokens = tokens;
355
96
  }
356
- const create = AgentClass.create;
357
- const agent = typeof create === "function" ? await create.call(AgentClass, { local: { cwd: job.cwd, enableAgentRetries: false } }) : typeof AgentClass === "function" ? new AgentClass : undefined;
358
- if (!agent || typeof agent.send !== "function") {
359
- throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
97
+ peek() {
98
+ return this.tokens[this.pos];
360
99
  }
361
- let run = await agent.send(job.prompt, { model: job.model });
362
- if (run && typeof run === "object") {
363
- const handle = run;
364
- if (typeof handle.cancel === "function")
365
- onCancel(() => {
366
- handle.cancel().catch(() => {});
367
- });
368
- if (typeof handle.wait === "function")
369
- run = await handle.wait();
100
+ next() {
101
+ return this.tokens[this.pos++];
370
102
  }
371
- if (!run || typeof run !== "object") {
372
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
103
+ expectOp(value) {
104
+ const t = this.next();
105
+ if (!t || t.kind !== "op" || t.value !== value) {
106
+ throw new ExprSyntaxError(`expected ${JSON.stringify(value)}`);
107
+ }
373
108
  }
374
- const candidate = run;
375
- if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
376
- throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
109
+ parse() {
110
+ if (this.tokens.length === 0)
111
+ throw new ExprSyntaxError("empty expression");
112
+ const node = this.parseOr();
113
+ if (this.pos < this.tokens.length) {
114
+ throw new ExprSyntaxError(`unexpected trailing token ${JSON.stringify(this.peek().value)}`);
115
+ }
116
+ return node;
377
117
  }
378
- if (typeof candidate.result !== "string") {
379
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
118
+ parseOr() {
119
+ let left = this.parseAnd();
120
+ while (this.peek()?.kind === "ident" && this.peek().value === "or") {
121
+ this.next();
122
+ const right = this.parseAnd();
123
+ left = { kind: "or", left, right };
124
+ }
125
+ return left;
380
126
  }
381
- return { status: candidate.status, result: candidate.result };
382
- }
383
- async function runCursorCli(job, cliPath, started, timeoutMs) {
384
- try {
385
- const result = await spawnCli([cliPath, "--print", job.prompt, ...job.model ? ["--model", job.model] : []], {
386
- binaryName: cliPath,
387
- packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
388
- timeoutMs,
389
- env: job.env,
390
- cwd: job.cwd
391
- });
392
- return { ...result, durationMs: Date.now() - started };
393
- } catch (err) {
394
- if (err instanceof LaneDependencyMissingError) {
395
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
127
+ parseAnd() {
128
+ let left = this.parseNot();
129
+ while (this.peek()?.kind === "ident" && this.peek().value === "and") {
130
+ this.next();
131
+ const right = this.parseNot();
132
+ left = { kind: "and", left, right };
396
133
  }
397
- throw err;
134
+ return left;
135
+ }
136
+ parseNot() {
137
+ if (this.peek()?.kind === "ident" && this.peek().value === "not") {
138
+ this.next();
139
+ return { kind: "not", operand: this.parseNot() };
140
+ }
141
+ return this.parseComparison();
142
+ }
143
+ parseComparison() {
144
+ const left = this.parsePrimary();
145
+ const t = this.peek();
146
+ if (t && t.kind === "op" && COMPARISON_OPS.has(t.value)) {
147
+ this.next();
148
+ const right = this.parsePrimary();
149
+ return { kind: "comparison", op: t.value, left, right };
150
+ }
151
+ return left;
152
+ }
153
+ parsePrimary() {
154
+ const t = this.next();
155
+ if (!t)
156
+ throw new ExprSyntaxError("unexpected end of expression");
157
+ if (t.kind === "lparen") {
158
+ const inner = this.parseOr();
159
+ this.expectCloseParen();
160
+ return inner;
161
+ }
162
+ if (t.kind === "number") {
163
+ const value = Number(t.value);
164
+ if (!Number.isFinite(value))
165
+ throw new ExprSyntaxError(`invalid number ${JSON.stringify(t.value)}`);
166
+ return { kind: "literal", value };
167
+ }
168
+ if (t.kind === "string")
169
+ return { kind: "literal", value: t.value };
170
+ if (t.kind === "ident") {
171
+ if (t.value === "true")
172
+ return { kind: "literal", value: true };
173
+ if (t.value === "false")
174
+ return { kind: "literal", value: false };
175
+ return { kind: "path", segments: t.value.split(".") };
176
+ }
177
+ throw new ExprSyntaxError(`unexpected token ${JSON.stringify(t.value)}`);
178
+ }
179
+ expectCloseParen() {
180
+ const t = this.next();
181
+ if (!t || t.kind !== "rparen")
182
+ throw new ExprSyntaxError("expected )");
398
183
  }
399
184
  }
400
- var init_cursor = __esm(() => {
401
- init_types();
402
- });
403
-
404
- // src/lanes/grok.ts
405
- function createGrokAdapter(deps = {}) {
406
- return {
407
- kind: "grok",
408
- async probe() {
409
- const cliPath = deps.cliPath ?? "grok";
410
- if (binaryOnPath(cliPath)) {
411
- return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
185
+ function parseExpr(input) {
186
+ return new Parser(tokenize(input)).parse();
187
+ }
188
+ function resolvePath(segments, context) {
189
+ let current = context;
190
+ for (const segment of segments) {
191
+ if (current === null || typeof current !== "object")
192
+ return;
193
+ const record = current;
194
+ if (!(segment in record))
195
+ return;
196
+ current = record[segment];
197
+ }
198
+ if (typeof current === "number" || typeof current === "string" || typeof current === "boolean") {
199
+ return current;
200
+ }
201
+ return;
202
+ }
203
+ function evalNode(node, context) {
204
+ switch (node.kind) {
205
+ case "literal":
206
+ return node.value;
207
+ case "path":
208
+ return resolvePath(node.segments, context);
209
+ case "comparison": {
210
+ const left = evalNode(node.left, context);
211
+ const right = evalNode(node.right, context);
212
+ if (left === undefined || right === undefined) {
213
+ if (node.op === "==")
214
+ return left === undefined && right === undefined;
215
+ if (node.op === "!=")
216
+ return left !== right;
217
+ throw new ExprEvalError("cannot order-compare an unknown path");
412
218
  }
413
- return {
414
- kind: "grok",
415
- sdk: "no npm SDK (measured 2026-08-25)",
416
- wired: false,
417
- reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
418
- };
419
- },
420
- async run(job) {
421
- if (!job.prompt) {
422
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
219
+ if (typeof left !== typeof right) {
220
+ throw new ExprEvalError(`cannot compare ${JSON.stringify(left)} (${typeof left}) with ${JSON.stringify(right)} (${typeof right})`);
423
221
  }
424
- const started = Date.now();
425
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
426
- const cliPath = deps.cliPath ?? "grok";
427
- try {
428
- const result = await spawnCli([cliPath, job.prompt], {
429
- binaryName: cliPath,
430
- packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
431
- timeoutMs,
432
- env: job.env,
433
- cwd: job.cwd
434
- });
435
- return { ...result, durationMs: Date.now() - started };
436
- } catch (err) {
437
- if (err instanceof LaneDependencyMissingError) {
438
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
439
- }
440
- throw err;
222
+ switch (node.op) {
223
+ case "==":
224
+ return left === right;
225
+ case "!=":
226
+ return left !== right;
227
+ case "<":
228
+ return left < right;
229
+ case "<=":
230
+ return left <= right;
231
+ case ">":
232
+ return left > right;
233
+ case ">=":
234
+ return left >= right;
235
+ default:
236
+ throw new ExprEvalError(`unknown operator ${node.op}`);
237
+ }
238
+ }
239
+ case "and": {
240
+ const left = evalNode(node.left, context);
241
+ const right = evalNode(node.right, context);
242
+ if (typeof left !== "boolean" || typeof right !== "boolean") {
243
+ throw new ExprEvalError("and requires boolean operands");
441
244
  }
245
+ return left && right;
442
246
  }
443
- };
444
- }
445
- var init_grok = __esm(() => {
446
- init_types();
447
- });
448
-
449
- // src/lanes/index.ts
450
- var exports_lanes = {};
451
- __export(exports_lanes, {
452
- runLaneJob: () => runLaneJob,
453
- resolveLane: () => resolveLane,
454
- probeLane: () => probeLane,
455
- laneInventory: () => laneInventory,
456
- createGrokAdapter: () => createGrokAdapter,
457
- createCursorAdapter: () => createCursorAdapter,
458
- createCodexAdapter: () => createCodexAdapter,
459
- createClaudeAdapter: () => createClaudeAdapter,
460
- binaryOnPath: () => binaryOnPath,
461
- LaneDependencyMissingError: () => LaneDependencyMissingError,
462
- LaneAdapterShapeError: () => LaneAdapterShapeError,
463
- LANE_KINDS: () => LANE_KINDS2
464
- });
465
- function resolveLane(kind) {
466
- const adapter = adapters.get(kind);
467
- if (!adapter)
468
- throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
469
- return adapter;
470
- }
471
- async function runLaneJob(job) {
472
- return resolveLane(job.lane).run(job);
473
- }
474
- async function probeLane(kind) {
475
- return resolveLane(kind).probe();
247
+ case "or": {
248
+ const left = evalNode(node.left, context);
249
+ const right = evalNode(node.right, context);
250
+ if (typeof left !== "boolean" || typeof right !== "boolean") {
251
+ throw new ExprEvalError("or requires boolean operands");
252
+ }
253
+ return left || right;
254
+ }
255
+ case "not": {
256
+ const operand = evalNode(node.operand, context);
257
+ if (typeof operand !== "boolean")
258
+ throw new ExprEvalError("not requires a boolean operand");
259
+ return !operand;
260
+ }
261
+ }
476
262
  }
477
- async function laneInventory() {
478
- return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
263
+ function evaluateExpr(input, context) {
264
+ const ast = parseExpr(input);
265
+ const value = evalNode(ast, context);
266
+ if (typeof value !== "boolean") {
267
+ throw new ExprEvalError(`condition must evaluate to a boolean, got ${typeof value}`);
268
+ }
269
+ return value;
479
270
  }
480
- var adapters;
481
- var init_lanes = __esm(() => {
482
- init_claude();
483
- init_codex();
484
- init_cursor();
485
- init_grok();
486
- init_types();
487
- init_types();
488
- init_claude();
489
- init_codex();
490
- init_cursor();
491
- init_grok();
492
- adapters = new Map([
493
- ["claude", createClaudeAdapter()],
494
- ["codex", createCodexAdapter()],
495
- ["cursor", createCursorAdapter()],
496
- ["grok", createGrokAdapter()]
497
- ]);
271
+ var ExprSyntaxError, ExprEvalError, COMPARISON_OPS;
272
+ var init_expr = __esm(() => {
273
+ ExprSyntaxError = class ExprSyntaxError extends Error {
274
+ constructor(message) {
275
+ super(message);
276
+ this.name = "ExprSyntaxError";
277
+ }
278
+ };
279
+ ExprEvalError = class ExprEvalError extends Error {
280
+ constructor(message) {
281
+ super(message);
282
+ this.name = "ExprEvalError";
283
+ }
284
+ };
285
+ COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
498
286
  });
499
287
 
500
- // src/expr.ts
501
- class ExprSyntaxError extends Error {
502
- constructor(message) {
503
- super(message);
504
- this.name = "ExprSyntaxError";
505
- }
288
+ // src/graph.ts
289
+ function issue(issues, path, message) {
290
+ issues.push({ path, message });
506
291
  }
507
-
508
- class ExprEvalError extends Error {
509
- constructor(message) {
510
- super(message);
511
- this.name = "ExprEvalError";
512
- }
292
+ function findNode(graph, id) {
293
+ return graph.nodes.find((n) => n.id === id);
513
294
  }
514
- var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
515
- function tokenize(input) {
516
- const tokens = [];
517
- let i = 0;
518
- while (i < input.length) {
519
- const ch = input[i];
520
- if (ch === " " || ch === "\t" || ch === `
521
- ` || ch === "\r") {
522
- i++;
523
- continue;
295
+ function collectEdges(graph) {
296
+ const edges = [];
297
+ for (const node of graph.nodes) {
298
+ switch (node.type) {
299
+ case "start":
300
+ case "step":
301
+ if (node.next)
302
+ edges.push({ from: node.id, to: node.next });
303
+ break;
304
+ case "decision":
305
+ if (node.then)
306
+ edges.push({ from: node.id, to: node.then });
307
+ if (node.else)
308
+ edges.push({ from: node.id, to: node.else });
309
+ break;
310
+ case "while":
311
+ if (node.next)
312
+ edges.push({ from: node.id, to: node.next });
313
+ break;
314
+ case "end":
315
+ break;
524
316
  }
525
- if (ch === "(") {
526
- tokens.push({ kind: "lparen", value: "(" });
527
- i++;
317
+ }
318
+ return edges;
319
+ }
320
+ function reachableNodes(graph) {
321
+ const start = graph.nodes.find((n) => n.type === "start");
322
+ if (!start)
323
+ return [];
324
+ const seen = new Set;
325
+ const stack = [start.id];
326
+ while (stack.length > 0) {
327
+ const id = stack.pop();
328
+ if (seen.has(id))
528
329
  continue;
529
- }
530
- if (ch === ")") {
531
- tokens.push({ kind: "rparen", value: ")" });
532
- i++;
330
+ seen.add(id);
331
+ const node = findNode(graph, id);
332
+ if (!node)
533
333
  continue;
334
+ if (node.type === "start" || node.type === "step") {
335
+ if (node.next)
336
+ stack.push(node.next);
337
+ } else if (node.type === "decision") {
338
+ if (node.then)
339
+ stack.push(node.then);
340
+ if (node.else)
341
+ stack.push(node.else);
342
+ } else if (node.type === "while") {
343
+ for (const bodyId of node.body)
344
+ stack.push(bodyId);
345
+ if (node.next)
346
+ stack.push(node.next);
534
347
  }
535
- if (ch === '"' || ch === "'") {
536
- const quote = ch;
537
- let j = i + 1;
538
- let out = "";
539
- let closed = false;
540
- while (j < input.length) {
541
- if (input[j] === quote) {
542
- closed = true;
543
- break;
544
- }
545
- out += input[j];
546
- j++;
348
+ }
349
+ return [...seen];
350
+ }
351
+ function findCycles(graph) {
352
+ const edges = collectEdges(graph);
353
+ const adj = new Map;
354
+ for (const e of edges) {
355
+ const list = adj.get(e.from) ?? [];
356
+ list.push(e.to);
357
+ adj.set(e.from, list);
358
+ }
359
+ const color = new Map;
360
+ const stack = [];
361
+ const cycles = [];
362
+ function visit(id) {
363
+ color.set(id, "grey");
364
+ stack.push(id);
365
+ for (const to of adj.get(id) ?? []) {
366
+ const c = color.get(to);
367
+ if (c === "grey") {
368
+ const idx = stack.indexOf(to);
369
+ cycles.push([...stack.slice(idx), to]);
370
+ } else if (c === undefined) {
371
+ visit(to);
547
372
  }
548
- if (!closed)
549
- throw new ExprSyntaxError(`unterminated string literal at offset ${i}`);
550
- tokens.push({ kind: "string", value: out });
551
- i = j + 1;
552
- continue;
553
- }
554
- const two = input.slice(i, i + 2);
555
- if (COMPARISON_OPS.has(two)) {
556
- tokens.push({ kind: "op", value: two });
557
- i += 2;
558
- continue;
559
- }
560
- if (ch === "<" || ch === ">" || ch === "=" || ch === "!") {
561
- tokens.push({ kind: "op", value: ch });
562
- i++;
563
- continue;
564
- }
565
- if (/[0-9]/.test(ch)) {
566
- let j = i;
567
- while (j < input.length && /[0-9.]/.test(input[j]))
568
- j++;
569
- tokens.push({ kind: "number", value: input.slice(i, j) });
570
- i = j;
571
- continue;
572
- }
573
- if (/[A-Za-z_]/.test(ch)) {
574
- let j = i;
575
- while (j < input.length && /[A-Za-z0-9_.]/.test(input[j]))
576
- j++;
577
- tokens.push({ kind: "ident", value: input.slice(i, j) });
578
- i = j;
579
- continue;
580
373
  }
581
- throw new ExprSyntaxError(`unexpected character ${JSON.stringify(ch)} at offset ${i}`);
374
+ stack.pop();
375
+ color.set(id, "black");
582
376
  }
583
- return tokens;
377
+ for (const node of graph.nodes) {
378
+ if (color.get(node.id) === undefined)
379
+ visit(node.id);
380
+ }
381
+ return cycles;
584
382
  }
585
-
586
- class Parser {
587
- tokens;
588
- pos = 0;
589
- constructor(tokens) {
590
- this.tokens = tokens;
383
+ function validateGraph(graph) {
384
+ const issues = [];
385
+ if (graph === null || typeof graph !== "object" || Array.isArray(graph)) {
386
+ return { ok: false, issues: [{ path: "graph", message: "graph must be an object" }] };
591
387
  }
592
- peek() {
593
- return this.tokens[this.pos];
388
+ if (typeof graph.name !== "string" || graph.name.trim() === "") {
389
+ issue(issues, "name", "graph requires a non-empty name");
390
+ }
391
+ if (typeof graph.version !== "string" || graph.version.trim() === "") {
392
+ issue(issues, "version", "graph requires a version string");
393
+ }
394
+ if (graph.memoWatch !== undefined) {
395
+ if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
396
+ issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
397
+ } else {
398
+ for (const entry of graph.memoWatch) {
399
+ if (typeof entry !== "string" || entry.trim() === "") {
400
+ issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
401
+ break;
402
+ }
403
+ }
404
+ }
594
405
  }
595
- next() {
596
- return this.tokens[this.pos++];
406
+ if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
407
+ issue(issues, "nodes", "graph requires at least one node");
408
+ return { ok: false, issues };
597
409
  }
598
- expectOp(value) {
599
- const t = this.next();
600
- if (!t || t.kind !== "op" || t.value !== value) {
601
- throw new ExprSyntaxError(`expected ${JSON.stringify(value)}`);
410
+ for (let index = 0;index < graph.nodes.length; index++) {
411
+ const node = graph.nodes[index];
412
+ if (node === null || typeof node !== "object" || Array.isArray(node)) {
413
+ issue(issues, `nodes.${index}`, "node must be an object");
414
+ continue;
602
415
  }
603
- }
604
- parse() {
605
- if (this.tokens.length === 0)
606
- throw new ExprSyntaxError("empty expression");
607
- const node = this.parseOr();
608
- if (this.pos < this.tokens.length) {
609
- throw new ExprSyntaxError(`unexpected trailing token ${JSON.stringify(this.peek().value)}`);
416
+ if (!GRAPH_NODE_TYPES.includes(node.type))
417
+ issue(issues, `nodes.${index}.type`, "type must be start, step, decision, while, or end");
418
+ for (const key of ["next", "then", "else", "prompt", "command"]) {
419
+ const value = node[key];
420
+ if (value !== undefined && (typeof value !== "string" || value.trim() === ""))
421
+ issue(issues, `nodes.${index}.${key}`, `${key} must be a non-empty string`);
610
422
  }
611
- return node;
423
+ if (node.type === "while" && (!Array.isArray(node.body) || node.body.some((id) => typeof id !== "string")))
424
+ issue(issues, `nodes.${index}.body`, "body must be an array of node ids");
612
425
  }
613
- parseOr() {
614
- let left = this.parseAnd();
615
- while (this.peek()?.kind === "ident" && this.peek().value === "or") {
616
- this.next();
617
- const right = this.parseAnd();
618
- left = { kind: "or", left, right };
426
+ if (issues.length)
427
+ return { ok: false, issues };
428
+ const starts = graph.nodes.filter((n) => n.type === "start");
429
+ if (starts.length === 0)
430
+ issue(issues, "nodes", "graph requires exactly one start node (found none)");
431
+ if (starts.length > 1)
432
+ issue(issues, "nodes", "graph requires exactly one start node (found multiple)");
433
+ const ends = graph.nodes.filter((n) => n.type === "end");
434
+ if (ends.length === 0)
435
+ issue(issues, "nodes", "graph requires at least one end node");
436
+ const seenIds = new Map;
437
+ for (const node of graph.nodes) {
438
+ if (typeof node.id !== "string" || !NODE_ID_RE.test(node.id)) {
439
+ issue(issues, "nodes", `node id ${JSON.stringify(node.id)} must match ${String(NODE_ID_RE)}`);
440
+ continue;
619
441
  }
620
- return left;
621
- }
622
- parseAnd() {
623
- let left = this.parseNot();
624
- while (this.peek()?.kind === "ident" && this.peek().value === "and") {
625
- this.next();
626
- const right = this.parseNot();
627
- left = { kind: "and", left, right };
442
+ if (seenIds.has(node.id)) {
443
+ issue(issues, `nodes.${node.id}`, `duplicate node id ${node.id}`);
444
+ continue;
628
445
  }
629
- return left;
630
- }
631
- parseNot() {
632
- if (this.peek()?.kind === "ident" && this.peek().value === "not") {
633
- this.next();
634
- return { kind: "not", operand: this.parseNot() };
446
+ seenIds.set(node.id, node);
447
+ if (!GRAPH_NODE_TYPES.includes(node.type)) {
448
+ issue(issues, `nodes.${node.id}`, `unknown node type ${JSON.stringify(node.type)}`);
449
+ continue;
635
450
  }
636
- return this.parseComparison();
451
+ validateNode(graph, node, issues);
637
452
  }
638
- parseComparison() {
639
- const left = this.parsePrimary();
640
- const t = this.peek();
641
- if (t && t.kind === "op" && COMPARISON_OPS.has(t.value)) {
642
- this.next();
643
- const right = this.parsePrimary();
644
- return { kind: "comparison", op: t.value, left, right };
453
+ for (const node of graph.nodes) {
454
+ const edges = nodeEdges(node);
455
+ for (const { kind, target } of edges) {
456
+ if (!seenIds.has(target)) {
457
+ issue(issues, `nodes.${node.id}`, `${kind} edge targets nonexistent node ${target}`);
458
+ }
645
459
  }
646
- return left;
647
460
  }
648
- parsePrimary() {
649
- const t = this.next();
650
- if (!t)
651
- throw new ExprSyntaxError("unexpected end of expression");
652
- if (t.kind === "lparen") {
653
- const inner = this.parseOr();
654
- this.expectCloseParen();
655
- return inner;
656
- }
657
- if (t.kind === "number") {
658
- const value = Number(t.value);
659
- if (!Number.isFinite(value))
660
- throw new ExprSyntaxError(`invalid number ${JSON.stringify(t.value)}`);
661
- return { kind: "literal", value };
662
- }
663
- if (t.kind === "string")
664
- return { kind: "literal", value: t.value };
665
- if (t.kind === "ident") {
666
- if (t.value === "true")
667
- return { kind: "literal", value: true };
668
- if (t.value === "false")
669
- return { kind: "literal", value: false };
670
- return { kind: "path", segments: t.value.split(".") };
461
+ if (starts.length === 1) {
462
+ const reachable = new Set(reachableNodes(graph));
463
+ for (const node of graph.nodes) {
464
+ if (!reachable.has(node.id)) {
465
+ issue(issues, `nodes.${node.id}`, `node ${node.id} is unreachable from start`);
466
+ }
671
467
  }
672
- throw new ExprSyntaxError(`unexpected token ${JSON.stringify(t.value)}`);
673
468
  }
674
- expectCloseParen() {
675
- const t = this.next();
676
- if (!t || t.kind !== "rparen")
677
- throw new ExprSyntaxError("expected )");
469
+ const cycles = findCycles(graph);
470
+ for (const cycle of cycles) {
471
+ issue(issues, "nodes", `explicit cycle detected: ${cycle.join(" -> ")} (loop only via a while node)`);
678
472
  }
473
+ return { ok: issues.length === 0, issues };
679
474
  }
680
- function parseExpr(input) {
681
- return new Parser(tokenize(input)).parse();
682
- }
683
- function resolvePath(segments, context) {
684
- let current = context;
685
- for (const segment of segments) {
686
- if (current === null || typeof current !== "object")
687
- return;
688
- const record = current;
689
- if (!(segment in record))
690
- return;
691
- current = record[segment];
692
- }
693
- if (typeof current === "number" || typeof current === "string" || typeof current === "boolean") {
694
- return current;
475
+ function nodeEdges(node) {
476
+ switch (node.type) {
477
+ case "start":
478
+ case "step":
479
+ return node.next ? [{ kind: "next", target: node.next }] : [];
480
+ case "decision":
481
+ return [
482
+ ...node.then ? [{ kind: "then", target: node.then }] : [],
483
+ ...node.else ? [{ kind: "else", target: node.else }] : []
484
+ ];
485
+ case "while":
486
+ return node.next ? [{ kind: "next", target: node.next }] : [];
487
+ case "end":
488
+ return [];
489
+ default:
490
+ return [];
695
491
  }
696
- return;
697
492
  }
698
- function evalNode(node, context) {
699
- switch (node.kind) {
700
- case "literal":
701
- return node.value;
702
- case "path":
703
- return resolvePath(node.segments, context);
704
- case "comparison": {
705
- const left = evalNode(node.left, context);
706
- const right = evalNode(node.right, context);
707
- if (left === undefined || right === undefined) {
708
- if (node.op === "==")
709
- return left === undefined && right === undefined;
710
- if (node.op === "!=")
711
- return left !== right;
712
- throw new ExprEvalError("cannot order-compare an unknown path");
493
+ function validateNode(graph, node, issues) {
494
+ switch (node.type) {
495
+ case "start":
496
+ if (!node.next)
497
+ issue(issues, `nodes.${node.id}`, "start node requires a next edge");
498
+ break;
499
+ case "step": {
500
+ if (!node.prompt && !node.command) {
501
+ issue(issues, `nodes.${node.id}`, "step node requires prompt or command");
713
502
  }
714
- if (typeof left !== typeof right) {
715
- throw new ExprEvalError(`cannot compare ${JSON.stringify(left)} (${typeof left}) with ${JSON.stringify(right)} (${typeof right})`);
503
+ if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
504
+ issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
716
505
  }
717
- switch (node.op) {
718
- case "==":
719
- return left === right;
720
- case "!=":
721
- return left !== right;
722
- case "<":
723
- return left < right;
724
- case "<=":
725
- return left <= right;
726
- case ">":
727
- return left > right;
728
- case ">=":
729
- return left >= right;
730
- default:
731
- throw new ExprEvalError(`unknown operator ${node.op}`);
506
+ if (node.cwd !== undefined && (typeof node.cwd !== "string" || !node.cwd.trim() || node.cwd.includes("\x00"))) {
507
+ issue(issues, `nodes.${node.id}.cwd`, "cwd must be a non-empty path string without NUL characters on the executor");
508
+ }
509
+ if (node.codexMode !== undefined) {
510
+ if (!CODEX_EXECUTION_MODES.includes(node.codexMode)) {
511
+ issue(issues, `nodes.${node.id}.codexMode`, "codexMode must be repository or read-only");
512
+ } else if (node.lane !== "codex" || !node.prompt || node.command !== undefined) {
513
+ issue(issues, `nodes.${node.id}.codexMode`, "codexMode is supported only for a codex prompt step without command");
514
+ }
515
+ }
516
+ if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
517
+ issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
518
+ }
519
+ if (node.timeoutMs !== undefined && (!Number.isInteger(node.timeoutMs) || node.timeoutMs < 1)) {
520
+ issue(issues, `nodes.${node.id}`, "timeoutMs must be a positive integer");
732
521
  }
522
+ break;
733
523
  }
734
- case "and": {
735
- const left = evalNode(node.left, context);
736
- const right = evalNode(node.right, context);
737
- if (typeof left !== "boolean" || typeof right !== "boolean") {
738
- throw new ExprEvalError("and requires boolean operands");
524
+ case "decision": {
525
+ if (typeof node.condition !== "string" || node.condition.trim() === "") {
526
+ issue(issues, `nodes.${node.id}`, "decision node requires a condition expression");
527
+ } else {
528
+ try {
529
+ parseExpr(node.condition);
530
+ } catch (err) {
531
+ if (err instanceof ExprSyntaxError) {
532
+ issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
533
+ } else {
534
+ throw err;
535
+ }
536
+ }
537
+ }
538
+ if (!node.then && !node.else) {
539
+ issue(issues, `nodes.${node.id}`, "decision node requires then and/or else edge");
540
+ }
541
+ break;
542
+ }
543
+ case "while": {
544
+ if (typeof node.condition !== "string" || node.condition.trim() === "") {
545
+ issue(issues, `nodes.${node.id}`, "while node requires a condition expression");
546
+ } else {
547
+ try {
548
+ parseExpr(node.condition);
549
+ } catch (err) {
550
+ if (err instanceof ExprSyntaxError) {
551
+ issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
552
+ } else {
553
+ throw err;
554
+ }
555
+ }
556
+ }
557
+ if (node.maxIterations === undefined) {
558
+ issue(issues, `nodes.${node.id}`, "while node requires maxIterations (finite loop bound)");
559
+ } else if (!Number.isInteger(node.maxIterations) || node.maxIterations < 1) {
560
+ issue(issues, `nodes.${node.id}`, "maxIterations must be a positive integer");
739
561
  }
740
- return left && right;
741
- }
742
- case "or": {
743
- const left = evalNode(node.left, context);
744
- const right = evalNode(node.right, context);
745
- if (typeof left !== "boolean" || typeof right !== "boolean") {
746
- throw new ExprEvalError("or requires boolean operands");
562
+ if (!Array.isArray(node.body) || node.body.length === 0) {
563
+ issue(issues, `nodes.${node.id}`, "while node requires a non-empty body");
564
+ } else {
565
+ for (const bodyId of node.body) {
566
+ const bodyNode = findNode(graph, bodyId);
567
+ if (!bodyNode) {
568
+ issue(issues, `nodes.${node.id}`, `while body references nonexistent node ${bodyId}`);
569
+ continue;
570
+ }
571
+ if (bodyNode.type === "start" || bodyNode.type === "end" || bodyNode.type === "while") {
572
+ issue(issues, `nodes.${node.id}`, `while body may only contain step/decision nodes in v1 (found ${bodyNode.type})`);
573
+ }
574
+ }
575
+ const bodySet = new Set(node.body);
576
+ for (const bodyId of node.body) {
577
+ const bodyNode = findNode(graph, bodyId);
578
+ if (!bodyNode)
579
+ continue;
580
+ for (const { kind, target } of nodeEdges(bodyNode)) {
581
+ if (!bodySet.has(target)) {
582
+ issue(issues, `nodes.${node.id}`, `while body node ${bodyId} ${kind}-edge leaves the body (target ${target})`);
583
+ }
584
+ }
585
+ }
747
586
  }
748
- return left || right;
749
- }
750
- case "not": {
751
- const operand = evalNode(node.operand, context);
752
- if (typeof operand !== "boolean")
753
- throw new ExprEvalError("not requires a boolean operand");
754
- return !operand;
587
+ break;
755
588
  }
589
+ case "end":
590
+ break;
756
591
  }
757
592
  }
758
- function evaluateExpr(input, context) {
759
- const ast = parseExpr(input);
760
- const value = evalNode(ast, context);
761
- if (typeof value !== "boolean") {
762
- throw new ExprEvalError(`condition must evaluate to a boolean, got ${typeof value}`);
593
+ var GRAPH_NODE_TYPES, LANE_KINDS, CODEX_EXECUTION_MODES, NODE_ID_RE;
594
+ var init_graph = __esm(() => {
595
+ init_expr();
596
+ GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
597
+ LANE_KINDS = ["claude", "codex", "cursor", "grok"];
598
+ CODEX_EXECUTION_MODES = ["repository", "read-only"];
599
+ NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
600
+ });
601
+
602
+ // src/lanes/types.ts
603
+ import { statSync } from "fs";
604
+ function binaryOnPath(binary) {
605
+ const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
606
+ for (const entry of pathEntries) {
607
+ try {
608
+ const stat = statSync(`${entry}/${binary}`);
609
+ if (stat.isFile())
610
+ return true;
611
+ } catch {}
763
612
  }
764
- return value;
613
+ return false;
765
614
  }
766
-
767
- // src/graph.ts
768
- var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
769
- var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
770
- var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
771
- function issue(issues, path, message) {
772
- issues.push({ path, message });
615
+ function subscriptionEnvironment(overrides = {}) {
616
+ const paid = /^(OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CODEX_API_KEY|CURSOR_API_KEY)$/;
617
+ const sensitive = /(?:API[_-]?KEY|SECRET|PASSWORD|TOKEN|DATABASE_URL|CREDENTIAL)/i;
618
+ const env = {};
619
+ for (const [name, value] of Object.entries(process.env)) {
620
+ if (value !== undefined && !paid.test(name) && (!sensitive.test(name) || name === "CLAUDE_CODE_OAUTH_TOKEN"))
621
+ env[name] = value;
622
+ }
623
+ for (const [name, value] of Object.entries(overrides)) {
624
+ if (paid.test(name) || sensitive.test(name))
625
+ throw new Error(`lane environment field ${name} requires a supported scoped credential consumer`);
626
+ env[name] = value;
627
+ }
628
+ return env;
773
629
  }
774
- function findNode(graph, id) {
775
- return graph.nodes.find((n) => n.id === id);
630
+ function subscriptionSdkEnvironment(overrides = {}) {
631
+ const safe = subscriptionEnvironment(overrides);
632
+ return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
776
633
  }
777
- function collectEdges(graph) {
778
- const edges = [];
779
- for (const node of graph.nodes) {
780
- switch (node.type) {
781
- case "start":
782
- case "step":
783
- if (node.next)
784
- edges.push({ from: node.id, to: node.next });
785
- break;
786
- case "decision":
787
- if (node.then)
788
- edges.push({ from: node.id, to: node.then });
789
- if (node.else)
790
- edges.push({ from: node.id, to: node.else });
791
- break;
792
- case "while":
793
- if (node.next)
794
- edges.push({ from: node.id, to: node.next });
795
- break;
796
- case "end":
797
- break;
634
+ function withTimeout(promise, timeoutMs, what, cancel) {
635
+ return new Promise((resolve, reject) => {
636
+ const timer = setTimeout(() => {
637
+ cancel?.();
638
+ reject(new LaneTimeoutError(what, timeoutMs));
639
+ }, timeoutMs);
640
+ promise.then((value) => {
641
+ clearTimeout(timer);
642
+ resolve(value);
643
+ }, (err) => {
644
+ clearTimeout(timer);
645
+ reject(err);
646
+ });
647
+ });
648
+ }
649
+ async function spawnCli(cmd, opts) {
650
+ const started = Date.now();
651
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
652
+ let proc;
653
+ try {
654
+ proc = Bun.spawn({
655
+ cmd,
656
+ cwd: opts.cwd,
657
+ env: subscriptionEnvironment(opts.env),
658
+ stdin: "ignore",
659
+ stdout: "pipe",
660
+ stderr: "pipe",
661
+ timeout: timeoutMs
662
+ });
663
+ } catch (err) {
664
+ const message = String(err instanceof Error ? err.message : err);
665
+ if (message.includes("ENOENT") || /not found/i.test(message)) {
666
+ throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
798
667
  }
668
+ return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
799
669
  }
800
- return edges;
670
+ const [stdout, stderr, exitCode] = await Promise.all([
671
+ new Response(proc.stdout).text(),
672
+ new Response(proc.stderr).text(),
673
+ proc.exited
674
+ ]);
675
+ const output = opts.stdoutOnly ? stdout.trim() : stderr ? `${stdout}
676
+ ${stderr}`.trim() : stdout.trim();
677
+ return {
678
+ ok: exitCode === 0,
679
+ exitCode,
680
+ output,
681
+ durationMs: Date.now() - started,
682
+ ...opts.stdoutOnly && exitCode !== 0 && stderr.trim() ? { error: stderr.trim() } : {}
683
+ };
801
684
  }
802
- function reachableNodes(graph) {
803
- const start = graph.nodes.find((n) => n.type === "start");
804
- if (!start)
805
- return [];
806
- const seen = new Set;
807
- const stack = [start.id];
808
- while (stack.length > 0) {
809
- const id = stack.pop();
810
- if (seen.has(id))
811
- continue;
812
- seen.add(id);
813
- const node = findNode(graph, id);
814
- if (!node)
815
- continue;
816
- if (node.type === "start" || node.type === "step") {
817
- if (node.next)
818
- stack.push(node.next);
819
- } else if (node.type === "decision") {
820
- if (node.then)
821
- stack.push(node.then);
822
- if (node.else)
823
- stack.push(node.else);
824
- } else if (node.type === "while") {
825
- for (const bodyId of node.body)
826
- stack.push(bodyId);
827
- if (node.next)
828
- stack.push(node.next);
685
+ var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000, LaneTimeoutError;
686
+ var init_types = __esm(() => {
687
+ LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
688
+ LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
689
+ constructor(message) {
690
+ super(message);
691
+ this.name = "LaneDependencyMissingError";
692
+ }
693
+ };
694
+ LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
695
+ constructor(message) {
696
+ super(message);
697
+ this.name = "LaneAdapterShapeError";
698
+ }
699
+ };
700
+ LaneTimeoutError = class LaneTimeoutError extends Error {
701
+ constructor(what, timeoutMs) {
702
+ super(`${what} exceeded the ${timeoutMs}ms budget`);
703
+ this.name = "LaneTimeoutError";
704
+ }
705
+ };
706
+ });
707
+
708
+ // src/lanes/claude.ts
709
+ function createClaudeAdapter(deps = {}) {
710
+ return {
711
+ kind: "claude",
712
+ async probe() {
713
+ const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
714
+ try {
715
+ await loader();
716
+ return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
717
+ } catch {
718
+ if (binaryOnPath(deps.cliPath ?? "claude")) {
719
+ return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
720
+ }
721
+ return {
722
+ kind: "claude",
723
+ sdk: "@anthropic-ai/claude-agent-sdk",
724
+ wired: false,
725
+ reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
726
+ };
727
+ }
728
+ },
729
+ async run(job) {
730
+ if (!job.prompt) {
731
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
732
+ }
733
+ const started = Date.now();
734
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
735
+ const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
736
+ let sdk;
737
+ try {
738
+ sdk = await loader();
739
+ } catch (err) {
740
+ return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
741
+ }
742
+ const query = sdk.query;
743
+ if (typeof query !== "function") {
744
+ throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
745
+ }
746
+ const prompt = job.prompt;
747
+ const abortController = new AbortController;
748
+ try {
749
+ const { output, isError } = await withTimeout((async () => {
750
+ const generator = await query({
751
+ prompt,
752
+ options: { model: job.model, cwd: job.cwd, maxTurns: 32, abortController, env: subscriptionSdkEnvironment(job.env) }
753
+ });
754
+ let output2 = "";
755
+ let isError2 = false;
756
+ for await (const message of generator) {
757
+ if (message.type === "assistant") {
758
+ const content = message.message?.content;
759
+ if (Array.isArray(content)) {
760
+ for (const block of content) {
761
+ if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
762
+ output2 += block.text;
763
+ }
764
+ }
765
+ }
766
+ }
767
+ if (message.type === "error")
768
+ isError2 = true;
769
+ }
770
+ return { output: output2, isError: isError2 };
771
+ })(), timeoutMs, "claude lane", () => abortController.abort());
772
+ const durationMs = Date.now() - started;
773
+ return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
774
+ } catch (err) {
775
+ const durationMs = Date.now() - started;
776
+ return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
777
+ }
829
778
  }
830
- }
831
- return [...seen];
779
+ };
832
780
  }
833
- function findCycles(graph) {
834
- const edges = collectEdges(graph);
835
- const adj = new Map;
836
- for (const e of edges) {
837
- const list = adj.get(e.from) ?? [];
838
- list.push(e.to);
839
- adj.set(e.from, list);
840
- }
841
- const color = new Map;
842
- const stack = [];
843
- const cycles = [];
844
- function visit(id) {
845
- color.set(id, "grey");
846
- stack.push(id);
847
- for (const to of adj.get(id) ?? []) {
848
- const c = color.get(to);
849
- if (c === "grey") {
850
- const idx = stack.indexOf(to);
851
- cycles.push([...stack.slice(idx), to]);
852
- } else if (c === undefined) {
853
- visit(to);
854
- }
781
+ async function runClaudeCli(job, cliPath, started, timeoutMs) {
782
+ try {
783
+ const result = await spawnCli([cliPath, "-p", job.prompt], {
784
+ binaryName: cliPath,
785
+ packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
786
+ timeoutMs,
787
+ env: job.env,
788
+ cwd: job.cwd
789
+ });
790
+ return { ...result, durationMs: Date.now() - started };
791
+ } catch (err) {
792
+ if (err instanceof LaneDependencyMissingError) {
793
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
855
794
  }
856
- stack.pop();
857
- color.set(id, "black");
858
- }
859
- for (const node of graph.nodes) {
860
- if (color.get(node.id) === undefined)
861
- visit(node.id);
795
+ throw err;
862
796
  }
863
- return cycles;
864
797
  }
865
- function validateGraph(graph) {
866
- const issues = [];
867
- if (graph === null || typeof graph !== "object" || Array.isArray(graph)) {
868
- return { ok: false, issues: [{ path: "graph", message: "graph must be an object" }] };
869
- }
870
- if (typeof graph.name !== "string" || graph.name.trim() === "") {
871
- issue(issues, "name", "graph requires a non-empty name");
872
- }
873
- if (typeof graph.version !== "string" || graph.version.trim() === "") {
874
- issue(issues, "version", "graph requires a version string");
875
- }
876
- if (graph.memoWatch !== undefined) {
877
- if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
878
- issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
879
- } else {
880
- for (const entry of graph.memoWatch) {
881
- if (typeof entry !== "string" || entry.trim() === "") {
882
- issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
883
- break;
798
+ var init_claude = __esm(() => {
799
+ init_types();
800
+ });
801
+
802
+ // src/lanes/codex.ts
803
+ function createCodexAdapter(deps = {}) {
804
+ return {
805
+ kind: "codex",
806
+ async probe() {
807
+ const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
808
+ try {
809
+ await loader();
810
+ return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
811
+ } catch {
812
+ if (binaryOnPath(deps.cliPath ?? "codex")) {
813
+ return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
884
814
  }
815
+ return {
816
+ kind: "codex",
817
+ sdk: "@openai/codex-sdk",
818
+ wired: false,
819
+ reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
820
+ };
821
+ }
822
+ },
823
+ async run(job) {
824
+ if (!job.prompt) {
825
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
826
+ }
827
+ if (job.codexMode !== undefined && (!CODEX_EXECUTION_MODES.includes(job.codexMode) || job.lane !== "codex" || job.command !== undefined)) {
828
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codexMode must be repository or read-only on a codex prompt without command", retryable: false };
829
+ }
830
+ const started = Date.now();
831
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
832
+ const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
833
+ let sdk;
834
+ try {
835
+ sdk = await loader();
836
+ } catch {
837
+ return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
838
+ }
839
+ const CodexClass = sdk.Codex;
840
+ if (typeof CodexClass !== "function") {
841
+ throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
842
+ }
843
+ const abortController = new AbortController;
844
+ try {
845
+ const client = await withTimeout(Promise.resolve(new CodexClass({
846
+ config: { model: job.model },
847
+ env: subscriptionEnvironment(job.env)
848
+ })), timeoutMs, "codex lane");
849
+ const thread = client.startThread?.({
850
+ model: job.model,
851
+ workingDirectory: job.cwd,
852
+ ...job.codexMode === "read-only" ? { sandboxMode: "read-only", skipGitRepoCheck: true, approvalPolicy: "never" } : {}
853
+ });
854
+ if (!thread || typeof thread.run !== "function") {
855
+ throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
856
+ }
857
+ const turn = await withTimeout(thread.run(job.prompt, { signal: abortController.signal }), timeoutMs, "codex lane", () => abortController.abort());
858
+ const durationMs = Date.now() - started;
859
+ const output = turn?.finalResponse;
860
+ if (typeof output !== "string" || !output.trim()) {
861
+ return missingResponse(durationMs);
862
+ }
863
+ return { ok: true, exitCode: 0, output, durationMs };
864
+ } catch (err) {
865
+ const durationMs = Date.now() - started;
866
+ return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
885
867
  }
886
868
  }
887
- }
888
- if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
889
- issue(issues, "nodes", "graph requires at least one node");
890
- return { ok: false, issues };
891
- }
892
- for (let index = 0;index < graph.nodes.length; index++) {
893
- const node = graph.nodes[index];
894
- if (node === null || typeof node !== "object" || Array.isArray(node)) {
895
- issue(issues, `nodes.${index}`, "node must be an object");
896
- continue;
897
- }
898
- if (!GRAPH_NODE_TYPES.includes(node.type))
899
- issue(issues, `nodes.${index}.type`, "type must be start, step, decision, while, or end");
900
- for (const key of ["next", "then", "else", "prompt", "command"]) {
901
- const value = node[key];
902
- if (value !== undefined && (typeof value !== "string" || value.trim() === ""))
903
- issue(issues, `nodes.${index}.${key}`, `${key} must be a non-empty string`);
904
- }
905
- if (node.type === "while" && (!Array.isArray(node.body) || node.body.some((id) => typeof id !== "string")))
906
- issue(issues, `nodes.${index}.body`, "body must be an array of node ids");
907
- }
908
- if (issues.length)
909
- return { ok: false, issues };
910
- const starts = graph.nodes.filter((n) => n.type === "start");
911
- if (starts.length === 0)
912
- issue(issues, "nodes", "graph requires exactly one start node (found none)");
913
- if (starts.length > 1)
914
- issue(issues, "nodes", "graph requires exactly one start node (found multiple)");
915
- const ends = graph.nodes.filter((n) => n.type === "end");
916
- if (ends.length === 0)
917
- issue(issues, "nodes", "graph requires at least one end node");
918
- const seenIds = new Map;
919
- for (const node of graph.nodes) {
920
- if (typeof node.id !== "string" || !NODE_ID_RE.test(node.id)) {
921
- issue(issues, "nodes", `node id ${JSON.stringify(node.id)} must match ${String(NODE_ID_RE)}`);
922
- continue;
923
- }
924
- if (seenIds.has(node.id)) {
925
- issue(issues, `nodes.${node.id}`, `duplicate node id ${node.id}`);
926
- continue;
927
- }
928
- seenIds.set(node.id, node);
929
- if (!GRAPH_NODE_TYPES.includes(node.type)) {
930
- issue(issues, `nodes.${node.id}`, `unknown node type ${JSON.stringify(node.type)}`);
931
- continue;
869
+ };
870
+ }
871
+ async function runCodexCli(job, cliPath, started, timeoutMs) {
872
+ try {
873
+ const controls = job.codexMode === "read-only" ? ["--sandbox", "read-only", "--skip-git-repo-check", "--config", 'approval_policy="never"'] : [];
874
+ const result = await spawnCli([cliPath, "exec", ...controls, "--", job.prompt], {
875
+ binaryName: cliPath,
876
+ packageHint: "@openai/codex-sdk or the codex CLI",
877
+ timeoutMs,
878
+ env: job.env,
879
+ cwd: job.cwd,
880
+ stdoutOnly: true
881
+ });
882
+ const durationMs = Date.now() - started;
883
+ return result.ok && !result.output.trim() ? missingResponse(durationMs) : { ...result, durationMs };
884
+ } catch (err) {
885
+ if (err instanceof LaneDependencyMissingError) {
886
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
932
887
  }
933
- validateNode(graph, node, issues);
888
+ throw err;
934
889
  }
935
- for (const node of graph.nodes) {
936
- const edges = nodeEdges(node);
937
- for (const { kind, target } of edges) {
938
- if (!seenIds.has(target)) {
939
- issue(issues, `nodes.${node.id}`, `${kind} edge targets nonexistent node ${target}`);
890
+ }
891
+ function missingResponse(durationMs) {
892
+ return { ok: false, exitCode: 1, output: "", durationMs, error: "Codex completed without an agent response; verify the native subscription and model route", retryable: false };
893
+ }
894
+ var init_codex = __esm(() => {
895
+ init_graph();
896
+ init_types();
897
+ });
898
+
899
+ // src/lanes/cursor.ts
900
+ function createCursorAdapter(deps = {}) {
901
+ return {
902
+ kind: "cursor",
903
+ async probe() {
904
+ const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
905
+ try {
906
+ await loader();
907
+ return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
908
+ } catch {
909
+ if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
910
+ return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
911
+ }
912
+ return {
913
+ kind: "cursor",
914
+ sdk: "@cursor/sdk",
915
+ wired: false,
916
+ reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
917
+ };
918
+ }
919
+ },
920
+ async run(job) {
921
+ if (!job.prompt) {
922
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
923
+ }
924
+ if (!deps.sdkLoader && binaryOnPath(deps.cliPath ?? "cursor-agent"))
925
+ return runCursorCli(job, deps.cliPath ?? "cursor-agent", Date.now(), job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS);
926
+ const started = Date.now();
927
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
928
+ const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
929
+ let sdk;
930
+ try {
931
+ sdk = await loader();
932
+ } catch {
933
+ return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
934
+ }
935
+ let cancel;
936
+ try {
937
+ subscriptionEnvironment(job.env);
938
+ if (!deps.sdkLoader && process.env.CURSOR_API_KEY)
939
+ throw new Error("cursor lane refuses ambient API billing; use the native subscription CLI");
940
+ const run = await withTimeout(runLocalAgent(sdk, job, (callback) => {
941
+ cancel = callback;
942
+ }), timeoutMs, "cursor lane", () => cancel?.());
943
+ const durationMs = Date.now() - started;
944
+ const status = run.status;
945
+ const ok = status === "finished";
946
+ return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
947
+ } catch (err) {
948
+ if (err instanceof LaneAdapterShapeError)
949
+ throw err;
950
+ const durationMs = Date.now() - started;
951
+ return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
940
952
  }
941
953
  }
954
+ };
955
+ }
956
+ async function runLocalAgent(sdk, job, onCancel) {
957
+ const AgentClass = sdk.Agent;
958
+ const CursorClass = sdk.Cursor;
959
+ if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
960
+ throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
942
961
  }
943
- if (starts.length === 1) {
944
- const reachable = new Set(reachableNodes(graph));
945
- for (const node of graph.nodes) {
946
- if (!reachable.has(node.id)) {
947
- issue(issues, `nodes.${node.id}`, `node ${node.id} is unreachable from start`);
948
- }
949
- }
962
+ const create = AgentClass.create;
963
+ const agent = typeof create === "function" ? await create.call(AgentClass, { local: { cwd: job.cwd, enableAgentRetries: false } }) : typeof AgentClass === "function" ? new AgentClass : undefined;
964
+ if (!agent || typeof agent.send !== "function") {
965
+ throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
950
966
  }
951
- const cycles = findCycles(graph);
952
- for (const cycle of cycles) {
953
- issue(issues, "nodes", `explicit cycle detected: ${cycle.join(" -> ")} (loop only via a while node)`);
967
+ let run = await agent.send(job.prompt, { model: job.model });
968
+ if (run && typeof run === "object") {
969
+ const handle = run;
970
+ if (typeof handle.cancel === "function")
971
+ onCancel(() => {
972
+ handle.cancel().catch(() => {});
973
+ });
974
+ if (typeof handle.wait === "function")
975
+ run = await handle.wait();
954
976
  }
955
- return { ok: issues.length === 0, issues };
956
- }
957
- function nodeEdges(node) {
958
- switch (node.type) {
959
- case "start":
960
- case "step":
961
- return node.next ? [{ kind: "next", target: node.next }] : [];
962
- case "decision":
963
- return [
964
- ...node.then ? [{ kind: "then", target: node.then }] : [],
965
- ...node.else ? [{ kind: "else", target: node.else }] : []
966
- ];
967
- case "while":
968
- return node.next ? [{ kind: "next", target: node.next }] : [];
969
- case "end":
970
- return [];
971
- default:
972
- return [];
977
+ if (!run || typeof run !== "object") {
978
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
973
979
  }
980
+ const candidate = run;
981
+ if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
982
+ throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
983
+ }
984
+ if (typeof candidate.result !== "string") {
985
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
986
+ }
987
+ return { status: candidate.status, result: candidate.result };
974
988
  }
975
- function validateNode(graph, node, issues) {
976
- switch (node.type) {
977
- case "start":
978
- if (!node.next)
979
- issue(issues, `nodes.${node.id}`, "start node requires a next edge");
980
- break;
981
- case "step": {
982
- if (!node.prompt && !node.command) {
983
- issue(issues, `nodes.${node.id}`, "step node requires prompt or command");
984
- }
985
- if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
986
- issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
987
- }
988
- if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
989
- issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
990
- }
991
- if (node.timeoutMs !== undefined && (!Number.isInteger(node.timeoutMs) || node.timeoutMs < 1)) {
992
- issue(issues, `nodes.${node.id}`, "timeoutMs must be a positive integer");
993
- }
994
- break;
995
- }
996
- case "decision": {
997
- if (typeof node.condition !== "string" || node.condition.trim() === "") {
998
- issue(issues, `nodes.${node.id}`, "decision node requires a condition expression");
999
- } else {
1000
- try {
1001
- parseExpr(node.condition);
1002
- } catch (err) {
1003
- if (err instanceof ExprSyntaxError) {
1004
- issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
1005
- } else {
1006
- throw err;
1007
- }
1008
- }
1009
- }
1010
- if (!node.then && !node.else) {
1011
- issue(issues, `nodes.${node.id}`, "decision node requires then and/or else edge");
1012
- }
1013
- break;
989
+ async function runCursorCli(job, cliPath, started, timeoutMs) {
990
+ try {
991
+ const result = await spawnCli([cliPath, "--print", job.prompt, ...job.model ? ["--model", job.model] : []], {
992
+ binaryName: cliPath,
993
+ packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
994
+ timeoutMs,
995
+ env: job.env,
996
+ cwd: job.cwd
997
+ });
998
+ return { ...result, durationMs: Date.now() - started };
999
+ } catch (err) {
1000
+ if (err instanceof LaneDependencyMissingError) {
1001
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
1014
1002
  }
1015
- case "while": {
1016
- if (typeof node.condition !== "string" || node.condition.trim() === "") {
1017
- issue(issues, `nodes.${node.id}`, "while node requires a condition expression");
1018
- } else {
1019
- try {
1020
- parseExpr(node.condition);
1021
- } catch (err) {
1022
- if (err instanceof ExprSyntaxError) {
1023
- issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
1024
- } else {
1025
- throw err;
1026
- }
1027
- }
1003
+ throw err;
1004
+ }
1005
+ }
1006
+ var init_cursor = __esm(() => {
1007
+ init_types();
1008
+ });
1009
+
1010
+ // src/lanes/grok.ts
1011
+ function createGrokAdapter(deps = {}) {
1012
+ return {
1013
+ kind: "grok",
1014
+ async probe() {
1015
+ const cliPath = deps.cliPath ?? "grok";
1016
+ if (binaryOnPath(cliPath)) {
1017
+ return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
1028
1018
  }
1029
- if (node.maxIterations === undefined) {
1030
- issue(issues, `nodes.${node.id}`, "while node requires maxIterations (finite loop bound)");
1031
- } else if (!Number.isInteger(node.maxIterations) || node.maxIterations < 1) {
1032
- issue(issues, `nodes.${node.id}`, "maxIterations must be a positive integer");
1019
+ return {
1020
+ kind: "grok",
1021
+ sdk: "no npm SDK (measured 2026-08-25)",
1022
+ wired: false,
1023
+ reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
1024
+ };
1025
+ },
1026
+ async run(job) {
1027
+ if (!job.prompt) {
1028
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
1033
1029
  }
1034
- if (!Array.isArray(node.body) || node.body.length === 0) {
1035
- issue(issues, `nodes.${node.id}`, "while node requires a non-empty body");
1036
- } else {
1037
- for (const bodyId of node.body) {
1038
- const bodyNode = findNode(graph, bodyId);
1039
- if (!bodyNode) {
1040
- issue(issues, `nodes.${node.id}`, `while body references nonexistent node ${bodyId}`);
1041
- continue;
1042
- }
1043
- if (bodyNode.type === "start" || bodyNode.type === "end" || bodyNode.type === "while") {
1044
- issue(issues, `nodes.${node.id}`, `while body may only contain step/decision nodes in v1 (found ${bodyNode.type})`);
1045
- }
1046
- }
1047
- const bodySet = new Set(node.body);
1048
- for (const bodyId of node.body) {
1049
- const bodyNode = findNode(graph, bodyId);
1050
- if (!bodyNode)
1051
- continue;
1052
- for (const { kind, target } of nodeEdges(bodyNode)) {
1053
- if (!bodySet.has(target)) {
1054
- issue(issues, `nodes.${node.id}`, `while body node ${bodyId} ${kind}-edge leaves the body (target ${target})`);
1055
- }
1056
- }
1030
+ const started = Date.now();
1031
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
1032
+ const cliPath = deps.cliPath ?? "grok";
1033
+ try {
1034
+ const result = await spawnCli([cliPath, job.prompt], {
1035
+ binaryName: cliPath,
1036
+ packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
1037
+ timeoutMs,
1038
+ env: job.env,
1039
+ cwd: job.cwd
1040
+ });
1041
+ return { ...result, durationMs: Date.now() - started };
1042
+ } catch (err) {
1043
+ if (err instanceof LaneDependencyMissingError) {
1044
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
1057
1045
  }
1046
+ throw err;
1058
1047
  }
1059
- break;
1060
1048
  }
1061
- case "end":
1062
- break;
1063
- }
1049
+ };
1050
+ }
1051
+ var init_grok = __esm(() => {
1052
+ init_types();
1053
+ });
1054
+
1055
+ // src/lanes/index.ts
1056
+ var exports_lanes = {};
1057
+ __export(exports_lanes, {
1058
+ runLaneJob: () => runLaneJob,
1059
+ resolveLane: () => resolveLane,
1060
+ probeLane: () => probeLane,
1061
+ laneInventory: () => laneInventory,
1062
+ createGrokAdapter: () => createGrokAdapter,
1063
+ createCursorAdapter: () => createCursorAdapter,
1064
+ createCodexAdapter: () => createCodexAdapter,
1065
+ createClaudeAdapter: () => createClaudeAdapter,
1066
+ binaryOnPath: () => binaryOnPath,
1067
+ LaneDependencyMissingError: () => LaneDependencyMissingError,
1068
+ LaneAdapterShapeError: () => LaneAdapterShapeError,
1069
+ LANE_KINDS: () => LANE_KINDS2
1070
+ });
1071
+ function resolveLane(kind) {
1072
+ const adapter = adapters.get(kind);
1073
+ if (!adapter)
1074
+ throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
1075
+ return adapter;
1076
+ }
1077
+ async function runLaneJob(job) {
1078
+ return resolveLane(job.lane).run(job);
1079
+ }
1080
+ async function probeLane(kind) {
1081
+ return resolveLane(kind).probe();
1082
+ }
1083
+ async function laneInventory() {
1084
+ return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
1064
1085
  }
1086
+ var adapters;
1087
+ var init_lanes = __esm(() => {
1088
+ init_claude();
1089
+ init_codex();
1090
+ init_cursor();
1091
+ init_grok();
1092
+ init_types();
1093
+ init_types();
1094
+ init_claude();
1095
+ init_codex();
1096
+ init_cursor();
1097
+ init_grok();
1098
+ adapters = new Map([
1099
+ ["claude", createClaudeAdapter()],
1100
+ ["codex", createCodexAdapter()],
1101
+ ["cursor", createCursorAdapter()],
1102
+ ["grok", createGrokAdapter()]
1103
+ ]);
1104
+ });
1065
1105
 
1066
1106
  // src/agent-tools.ts
1107
+ init_graph();
1067
1108
  init_lanes();
1068
1109
 
1069
1110
  // src/tool-definitions.ts