@hasna/workflows 0.1.4 → 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,454 +16,7 @@ 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 as statSync2 } 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 = statSync2(`${entry}/${binary}`);
26
- if (stat.isFile())
27
- return true;
28
- } catch {}
29
- }
30
- return false;
31
- }
32
- function withTimeout(promise, timeoutMs, what) {
33
- return new Promise((resolve, reject) => {
34
- const timer = setTimeout(() => {
35
- reject(new Error(`${what} exceeded the ${timeoutMs}ms budget`));
36
- }, timeoutMs);
37
- promise.then((value) => {
38
- clearTimeout(timer);
39
- resolve(value);
40
- }, (err) => {
41
- clearTimeout(timer);
42
- reject(err);
43
- });
44
- });
45
- }
46
- function spawnCli(cmd, opts) {
47
- const started = Date.now();
48
- const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
49
- let proc;
50
- try {
51
- proc = Bun.spawnSync({
52
- cmd,
53
- cwd: opts.cwd,
54
- env: { ...process.env, ...opts.env ?? {} },
55
- stdout: "pipe",
56
- stderr: "pipe",
57
- timeout: timeoutMs
58
- });
59
- } catch (err) {
60
- const message = String(err instanceof Error ? err.message : err);
61
- if (message.includes("ENOENT") || /not found/i.test(message)) {
62
- throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
63
- }
64
- return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
65
- }
66
- const durationMs = Date.now() - started;
67
- const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
68
- const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
69
- const output = stderr ? `${stdout}
70
- ${stderr}`.trim() : stdout.trim();
71
- const exitCode = proc.exitCode ?? 1;
72
- return { ok: exitCode === 0, exitCode, output, durationMs };
73
- }
74
- var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000;
75
- var init_types = __esm(() => {
76
- LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
77
- LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
78
- constructor(message) {
79
- super(message);
80
- this.name = "LaneDependencyMissingError";
81
- }
82
- };
83
- LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
84
- constructor(message) {
85
- super(message);
86
- this.name = "LaneAdapterShapeError";
87
- }
88
- };
89
- });
90
-
91
- // src/lanes/claude.ts
92
- function createClaudeAdapter(deps = {}) {
93
- return {
94
- kind: "claude",
95
- async probe() {
96
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
97
- try {
98
- await loader();
99
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
100
- } catch {
101
- if (binaryOnPath(deps.cliPath ?? "claude")) {
102
- return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
103
- }
104
- return {
105
- kind: "claude",
106
- sdk: "@anthropic-ai/claude-agent-sdk",
107
- wired: false,
108
- reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
109
- };
110
- }
111
- },
112
- async run(job) {
113
- if (!job.prompt) {
114
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
115
- }
116
- const started = Date.now();
117
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
118
- const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
119
- let sdk;
120
- try {
121
- sdk = await loader();
122
- } catch (err) {
123
- return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
124
- }
125
- const query = sdk.query;
126
- if (typeof query !== "function") {
127
- throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
128
- }
129
- const prompt = job.prompt;
130
- try {
131
- const { output, isError } = await withTimeout((async () => {
132
- const generator = await query({
133
- prompt,
134
- options: { model: job.model, maxTurns: 32, abortSignal: undefined }
135
- });
136
- let output2 = "";
137
- let isError2 = false;
138
- for await (const message of generator) {
139
- if (message.type === "assistant") {
140
- const content = message.message?.content;
141
- if (Array.isArray(content)) {
142
- for (const block of content) {
143
- if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
144
- output2 += block.text;
145
- }
146
- }
147
- }
148
- }
149
- if (message.type === "error")
150
- isError2 = true;
151
- }
152
- return { output: output2, isError: isError2 };
153
- })(), timeoutMs, `claude lane (${job.prompt.slice(0, 60)})`);
154
- const durationMs = Date.now() - started;
155
- return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
156
- } catch (err) {
157
- const durationMs = Date.now() - started;
158
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
159
- }
160
- }
161
- };
162
- }
163
- function runClaudeCli(job, cliPath, started, timeoutMs) {
164
- try {
165
- const result = spawnCli([cliPath, "-p", job.prompt], {
166
- binaryName: cliPath,
167
- packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
168
- timeoutMs,
169
- env: job.env,
170
- cwd: job.cwd
171
- });
172
- return { ...result, durationMs: Date.now() - started };
173
- } catch (err) {
174
- if (err instanceof LaneDependencyMissingError) {
175
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
176
- }
177
- throw err;
178
- }
179
- }
180
- var init_claude = __esm(() => {
181
- init_types();
182
- });
183
-
184
- // src/lanes/codex.ts
185
- function createCodexAdapter(deps = {}) {
186
- return {
187
- kind: "codex",
188
- async probe() {
189
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
190
- try {
191
- await loader();
192
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
193
- } catch {
194
- if (binaryOnPath(deps.cliPath ?? "codex")) {
195
- return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
196
- }
197
- return {
198
- kind: "codex",
199
- sdk: "@openai/codex-sdk",
200
- wired: false,
201
- reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
202
- };
203
- }
204
- },
205
- async run(job) {
206
- if (!job.prompt) {
207
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
208
- }
209
- const started = Date.now();
210
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
211
- const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
212
- let sdk;
213
- try {
214
- sdk = await loader();
215
- } catch {
216
- return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
217
- }
218
- const CodexClass = sdk.Codex;
219
- if (typeof CodexClass !== "function") {
220
- throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
221
- }
222
- try {
223
- const client = await withTimeout(Promise.resolve(new CodexClass({
224
- config: { model: job.model },
225
- env: job.env
226
- })), timeoutMs, `codex lane (${job.prompt.slice(0, 60)})`);
227
- const thread = client.startThread?.();
228
- if (!thread || typeof thread.run !== "function") {
229
- throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
230
- }
231
- const turn = await withTimeout(thread.run(job.prompt), timeoutMs, `codex lane (${job.prompt.slice(0, 60)})`);
232
- const output = turn?.finalResponse ?? "";
233
- const durationMs = Date.now() - started;
234
- return { ok: true, exitCode: 0, output, durationMs };
235
- } catch (err) {
236
- const durationMs = Date.now() - started;
237
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
238
- }
239
- }
240
- };
241
- }
242
- function runCodexCli(job, cliPath, started, timeoutMs) {
243
- try {
244
- const result = spawnCli([cliPath, "exec", "--", job.prompt], {
245
- binaryName: cliPath,
246
- packageHint: "@openai/codex-sdk or the codex CLI",
247
- timeoutMs,
248
- env: job.env,
249
- cwd: job.cwd
250
- });
251
- return { ...result, durationMs: Date.now() - started };
252
- } catch (err) {
253
- if (err instanceof LaneDependencyMissingError) {
254
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
255
- }
256
- throw err;
257
- }
258
- }
259
- var init_codex = __esm(() => {
260
- init_types();
261
- });
262
-
263
- // src/lanes/cursor.ts
264
- function createCursorAdapter(deps = {}) {
265
- return {
266
- kind: "cursor",
267
- async probe() {
268
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
269
- try {
270
- await loader();
271
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
272
- } catch {
273
- if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
274
- return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
275
- }
276
- return {
277
- kind: "cursor",
278
- sdk: "@cursor/sdk",
279
- wired: false,
280
- reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
281
- };
282
- }
283
- },
284
- async run(job) {
285
- if (!job.prompt) {
286
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
287
- }
288
- const started = Date.now();
289
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
290
- const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
291
- let sdk;
292
- try {
293
- sdk = await loader();
294
- } catch {
295
- return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
296
- }
297
- try {
298
- const run = await withTimeout(runLocalAgent(sdk, job), timeoutMs, `cursor lane (${job.prompt.slice(0, 60)})`);
299
- const durationMs = Date.now() - started;
300
- const status = run.status;
301
- const ok = status === "finished";
302
- return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
303
- } catch (err) {
304
- if (err instanceof LaneAdapterShapeError)
305
- throw err;
306
- const durationMs = Date.now() - started;
307
- return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
308
- }
309
- }
310
- };
311
- }
312
- async function runLocalAgent(sdk, job) {
313
- const AgentClass = sdk.Agent;
314
- const CursorClass = sdk.Cursor;
315
- if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
316
- throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
317
- }
318
- const agent = typeof AgentClass === "function" ? new AgentClass : undefined;
319
- if (!agent || typeof agent.send !== "function") {
320
- throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
321
- }
322
- const run = await agent.send(job.prompt, { model: job.model });
323
- if (!run || typeof run !== "object") {
324
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
325
- }
326
- const candidate = run;
327
- if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
328
- throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
329
- }
330
- if (typeof candidate.result !== "string") {
331
- throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
332
- }
333
- return { status: candidate.status, result: candidate.result };
334
- }
335
- function runCursorCli(job, cliPath, started, timeoutMs) {
336
- try {
337
- const result = spawnCli([cliPath, "run", job.prompt], {
338
- binaryName: cliPath,
339
- packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
340
- timeoutMs,
341
- env: job.env,
342
- cwd: job.cwd
343
- });
344
- return { ...result, durationMs: Date.now() - started };
345
- } catch (err) {
346
- if (err instanceof LaneDependencyMissingError) {
347
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
348
- }
349
- throw err;
350
- }
351
- }
352
- var init_cursor = __esm(() => {
353
- init_types();
354
- });
355
-
356
- // src/lanes/grok.ts
357
- function createGrokAdapter(deps = {}) {
358
- return {
359
- kind: "grok",
360
- async probe() {
361
- const cliPath = deps.cliPath ?? "grok";
362
- if (binaryOnPath(cliPath)) {
363
- return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
364
- }
365
- return {
366
- kind: "grok",
367
- sdk: "no npm SDK (measured 2026-08-25)",
368
- wired: false,
369
- reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
370
- };
371
- },
372
- async run(job) {
373
- if (!job.prompt) {
374
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
375
- }
376
- const started = Date.now();
377
- const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
378
- const cliPath = deps.cliPath ?? "grok";
379
- try {
380
- const result = spawnCli([cliPath, job.prompt], {
381
- binaryName: cliPath,
382
- packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
383
- timeoutMs,
384
- env: job.env,
385
- cwd: job.cwd
386
- });
387
- return { ...result, durationMs: Date.now() - started };
388
- } catch (err) {
389
- if (err instanceof LaneDependencyMissingError) {
390
- return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
391
- }
392
- throw err;
393
- }
394
- }
395
- };
396
- }
397
- var init_grok = __esm(() => {
398
- init_types();
399
- });
400
-
401
- // src/lanes/index.ts
402
- var exports_lanes = {};
403
- __export(exports_lanes, {
404
- runLaneJob: () => runLaneJob,
405
- resolveLane: () => resolveLane,
406
- probeLane: () => probeLane,
407
- laneInventory: () => laneInventory,
408
- createGrokAdapter: () => createGrokAdapter,
409
- createCursorAdapter: () => createCursorAdapter,
410
- createCodexAdapter: () => createCodexAdapter,
411
- createClaudeAdapter: () => createClaudeAdapter,
412
- binaryOnPath: () => binaryOnPath,
413
- LaneDependencyMissingError: () => LaneDependencyMissingError,
414
- LaneAdapterShapeError: () => LaneAdapterShapeError,
415
- LANE_KINDS: () => LANE_KINDS2
416
- });
417
- function resolveLane(kind) {
418
- const adapter = adapters.get(kind);
419
- if (!adapter)
420
- throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
421
- return adapter;
422
- }
423
- async function runLaneJob(job) {
424
- return resolveLane(job.lane).run(job);
425
- }
426
- async function probeLane(kind) {
427
- return resolveLane(kind).probe();
428
- }
429
- async function laneInventory() {
430
- return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
431
- }
432
- var adapters;
433
- var init_lanes = __esm(() => {
434
- init_claude();
435
- init_codex();
436
- init_cursor();
437
- init_grok();
438
- init_types();
439
- init_types();
440
- init_claude();
441
- init_codex();
442
- init_cursor();
443
- init_grok();
444
- adapters = new Map([
445
- ["claude", createClaudeAdapter()],
446
- ["codex", createCodexAdapter()],
447
- ["cursor", createCursorAdapter()],
448
- ["grok", createGrokAdapter()]
449
- ]);
450
- });
451
-
452
19
  // src/expr.ts
453
- class ExprSyntaxError extends Error {
454
- constructor(message) {
455
- super(message);
456
- this.name = "ExprSyntaxError";
457
- }
458
- }
459
-
460
- class ExprEvalError extends Error {
461
- constructor(message) {
462
- super(message);
463
- this.name = "ExprEvalError";
464
- }
465
- }
466
- var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
467
20
  function tokenize(input) {
468
21
  const tokens = [];
469
22
  let i = 0;
@@ -715,11 +268,24 @@ function evaluateExpr(input, context) {
715
268
  }
716
269
  return value;
717
270
  }
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(["==", "!=", "<", "<=", ">", ">="]);
286
+ });
718
287
 
719
288
  // src/graph.ts
720
- var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
721
- var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
722
- var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
723
289
  function issue(issues, path, message) {
724
290
  issues.push({ path, message });
725
291
  }
@@ -816,6 +382,9 @@ function findCycles(graph) {
816
382
  }
817
383
  function validateGraph(graph) {
818
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" }] };
387
+ }
819
388
  if (typeof graph.name !== "string" || graph.name.trim() === "") {
820
389
  issue(issues, "name", "graph requires a non-empty name");
821
390
  }
@@ -838,6 +407,24 @@ function validateGraph(graph) {
838
407
  issue(issues, "nodes", "graph requires at least one node");
839
408
  return { ok: false, issues };
840
409
  }
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;
415
+ }
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`);
422
+ }
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");
425
+ }
426
+ if (issues.length)
427
+ return { ok: false, issues };
841
428
  const starts = graph.nodes.filter((n) => n.type === "start");
842
429
  if (starts.length === 0)
843
430
  issue(issues, "nodes", "graph requires exactly one start node (found none)");
@@ -899,6 +486,8 @@ function nodeEdges(node) {
899
486
  return node.next ? [{ kind: "next", target: node.next }] : [];
900
487
  case "end":
901
488
  return [];
489
+ default:
490
+ return [];
902
491
  }
903
492
  }
904
493
  function validateNode(graph, node, issues) {
@@ -914,6 +503,16 @@ function validateNode(graph, node, issues) {
914
503
  if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
915
504
  issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
916
505
  }
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
+ }
917
516
  if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
918
517
  issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
919
518
  }
@@ -973,1002 +572,547 @@ function validateNode(graph, node, issues) {
973
572
  issue(issues, `nodes.${node.id}`, `while body may only contain step/decision nodes in v1 (found ${bodyNode.type})`);
974
573
  }
975
574
  }
976
- const bodySet = new Set(node.body);
977
- for (const bodyId of node.body) {
978
- const bodyNode = findNode(graph, bodyId);
979
- if (!bodyNode)
980
- continue;
981
- for (const { kind, target } of nodeEdges(bodyNode)) {
982
- if (!bodySet.has(target)) {
983
- issue(issues, `nodes.${node.id}`, `while body node ${bodyId} ${kind}-edge leaves the body (target ${target})`);
984
- }
985
- }
986
- }
987
- }
988
- break;
989
- }
990
- case "end":
991
- break;
992
- }
993
- }
994
-
995
- // src/secrets.ts
996
- class SecretsGateError extends Error {
997
- constructor(message) {
998
- super(message);
999
- this.name = "SecretsGateError";
1000
- }
1001
- }
1002
- var p = (...parts) => parts.join("");
1003
- var VALUE_PATTERNS = [
1004
- { detector: "anthropic_api_key", re: new RegExp(p("sk-", "ant-") + "[A-Za-z0-9_-]{10,}") },
1005
- { detector: "openai_api_key", re: new RegExp(p("sk-", "proj-") + "[A-Za-z0-9_-]{10,}") },
1006
- { detector: "npm_token", re: /npm_[A-Za-z0-9]{20,}/ },
1007
- { detector: "github_token", re: /gh[pousr]_[A-Za-z0-9]{20,}/ },
1008
- { detector: "aws_access_key", re: /AKIA[0-9A-Z]{16}/ },
1009
- { detector: "google_api_key", re: /AIza[0-9A-Za-z_-]{20,}/ },
1010
- { detector: "xai_api_key", re: new RegExp(p("xai-") + "[A-Za-z0-9]{20,}") },
1011
- { detector: "codex_token", re: new RegExp(p("ctx7", "sk-") + "[A-Za-z0-9]{10,}") },
1012
- { detector: "private_key_block", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/ },
1013
- { detector: "bearer_token", re: /Bearer [A-Za-z0-9._~+/=-]{20,}/ }
1014
- ];
1015
- var CREDENTIAL_KEY_RE = /^(token|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|refresh[_-]?token|session[_-]?token|bot[_-]?token)$/i;
1016
- function scanString(value, path, findings) {
1017
- for (const { detector, re } of VALUE_PATTERNS) {
1018
- if (re.test(value)) {
1019
- findings.push({ path, detector });
1020
- return;
1021
- }
1022
- }
1023
- }
1024
- function isTrivial(value) {
1025
- const v = value.trim();
1026
- if (v.length === 0)
1027
- return true;
1028
- const upper = v.toUpperCase();
1029
- if (upper.startsWith("REPLACE") || upper.startsWith("CHANGE") || upper.startsWith("YOUR_") || upper.startsWith("EXAMPLE") || upper.startsWith("PLACEHOLDER"))
1030
- return true;
1031
- if (v.length < 8)
1032
- return true;
1033
- return false;
1034
- }
1035
- function scanForSecrets(value, path = "$") {
1036
- const findings = [];
1037
- if (value === null || value === undefined)
1038
- return findings;
1039
- if (typeof value === "string") {
1040
- scanString(value, path, findings);
1041
- return findings;
1042
- }
1043
- if (Array.isArray(value)) {
1044
- value.forEach((item, index) => {
1045
- findings.push(...scanForSecrets(item, `${path}[${index}]`));
1046
- });
1047
- return findings;
1048
- }
1049
- if (typeof value === "object") {
1050
- const record = value;
1051
- for (const [key, child] of Object.entries(record)) {
1052
- if (CREDENTIAL_KEY_RE.test(key) && typeof child === "string" && !isTrivial(child)) {
1053
- findings.push({ path: `${path}.${key}`, detector: "credential_key_field" });
1054
- } else {
1055
- findings.push(...scanForSecrets(child, `${path}.${key}`));
1056
- }
1057
- }
1058
- }
1059
- return findings;
1060
- }
1061
- function assertNoSecrets(value, what) {
1062
- const findings = scanForSecrets(value);
1063
- if (findings.length === 0)
1064
- return;
1065
- const detail = findings.map((f) => `${f.path} (${f.detector})`).join(", ");
1066
- throw new SecretsGateError(`secrets write-gate refused to persist ${what}: credential-shaped value at ${detail}`);
1067
- }
1068
-
1069
- // src/session.ts
1070
- import { createHash } from "crypto";
1071
- import { readFileSync, statSync } from "fs";
1072
- import { isAbsolute, join } from "path";
1073
- var DEFAULT_MAX_ATTEMPTS = 3;
1074
- function repairTornRuns(store, wal, opts = {}) {
1075
- const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1076
- const replay = wal.replay();
1077
- const liveClaims = replay.liveClaims(opts.now);
1078
- const report = { interrupted: 0, requeued: 0, failed: 0 };
1079
- const running = store.listRuns({ status: "running", limit: 1000 });
1080
- for (const run of running) {
1081
- if (liveClaims.has(run.id))
1082
- continue;
1083
- report.interrupted++;
1084
- const nodes = store.listRunNodes(run.id);
1085
- if (run.attempts < maxAttempts) {
1086
- store.bumpAttempts(run.id);
1087
- store.setRunStatus(run.id, "interrupted", {
1088
- error: `torn run interrupted after claim loss (attempt ${run.attempts + 1}/${maxAttempts})`
1089
- });
1090
- for (const node of nodes) {
1091
- if (node.status === "running") {
1092
- store.setRunNodeStatus(node.id, "pending", { error: undefined });
1093
- }
1094
- }
1095
- report.requeued++;
1096
- } else {
1097
- store.setRunStatus(run.id, "failed", {
1098
- error: `torn run failed after ${maxAttempts} interrupted attempts`
1099
- });
1100
- for (const node of nodes) {
1101
- if (node.status === "running") {
1102
- store.setRunNodeStatus(node.id, "failed", { error: "run interrupted beyond retry budget" });
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
+ }
1103
585
  }
1104
586
  }
1105
- report.failed++;
1106
- }
1107
- }
1108
- return report;
1109
- }
1110
- function inputHash(payload) {
1111
- return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
1112
- }
1113
- function memoKey(graphName, nodeId, input) {
1114
- return `${graphName}:${nodeId}:${inputHash(input)}`;
1115
- }
1116
- function tryMemoHit(store, graphName, nodeId, input) {
1117
- const key = memoKey(graphName, nodeId, input);
1118
- const memo = store.memoGet(key);
1119
- if (!memo)
1120
- return;
1121
- store.memoHit(key);
1122
- return memo;
1123
- }
1124
- function recordMemo(store, graphName, nodeId, input, output) {
1125
- const key = memoKey(graphName, nodeId, input);
1126
- store.memoPut(key, graphName, nodeId, inputHash(input), JSON.stringify(output));
1127
- }
1128
- function memoWatchFingerprint(graph, dataDir) {
1129
- if (!graph.memoWatch || graph.memoWatch.length === 0)
1130
- return;
1131
- const entries = [];
1132
- for (const pattern of graph.memoWatch) {
1133
- if (hasGlobMagic(pattern)) {
1134
- const matches = [...new Bun.Glob(pattern).scanSync({ cwd: dataDir, onlyFiles: true })].sort();
1135
- for (const match of matches) {
1136
- const path2 = isAbsolute(match) ? match : join(dataDir, match);
1137
- entries.push(fingerprintFile(pattern, path2));
1138
- }
1139
- continue;
1140
- }
1141
- const path = isAbsolute(pattern) ? pattern : join(dataDir, pattern);
1142
- entries.push(fingerprintFile(pattern, path));
1143
- }
1144
- return entries;
1145
- }
1146
- function hasGlobMagic(pattern) {
1147
- return /[*?[\]{}]/.test(pattern);
1148
- }
1149
- function fingerprintFile(pattern, path) {
1150
- try {
1151
- const stat = statSync(path);
1152
- if (!stat.isFile()) {
1153
- return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
587
+ break;
1154
588
  }
1155
- const content = readFileSync(path);
1156
- return {
1157
- pattern,
1158
- path,
1159
- missing: false,
1160
- mtimeMs: stat.mtimeMs,
1161
- size: content.byteLength,
1162
- sha256: createHash("sha256").update(content).digest("hex")
1163
- };
1164
- } catch {
1165
- return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
589
+ case "end":
590
+ break;
1166
591
  }
1167
592
  }
1168
- function findRunByIdempotencyKey(store, key) {
1169
- const runs = store.listRuns({ limit: 1000 });
1170
- for (const run of runs) {
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) {
1171
607
  try {
1172
- const context = JSON.parse(run.contextJson);
1173
- if (context?.__wf?.idempotencyKey === key)
1174
- return run;
608
+ const stat = statSync(`${entry}/${binary}`);
609
+ if (stat.isFile())
610
+ return true;
1175
611
  } catch {}
1176
612
  }
1177
- return;
613
+ return false;
1178
614
  }
1179
-
1180
- // src/daemon.ts
1181
- init_types();
1182
- var DEFAULT_LEASE_TTL_MS = 120000;
1183
- var DEFAULT_MAX_ATTEMPTS2 = 3;
1184
- var DEFAULT_MAX_DISPATCH = 4;
1185
- var DEFAULT_MAX_STEPS = 25;
1186
- var DEFAULT_TIMEOUT_MS = 120000;
1187
- function nowIso() {
1188
- return new Date().toISOString();
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;
1189
629
  }
1190
- function stripRunState(context) {
1191
- const { __wf: _wf, steps: _steps, ...user } = context;
1192
- return user;
630
+ function subscriptionSdkEnvironment(overrides = {}) {
631
+ const safe = subscriptionEnvironment(overrides);
632
+ return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
1193
633
  }
1194
- function runCommand(job) {
1195
- if (!job.command) {
1196
- return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "no command" };
1197
- }
1198
- const started = Date.now();
1199
- const timeoutMs = job.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1200
- const proc = Bun.spawnSync({
1201
- cmd: ["/bin/sh", "-c", job.command],
1202
- cwd: job.cwd,
1203
- env: { ...process.env, ...job.env ?? {} },
1204
- stdout: "pipe",
1205
- stderr: "pipe",
1206
- timeout: timeoutMs
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
+ });
1207
647
  });
1208
- const durationMs = Date.now() - started;
1209
- const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
1210
- const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
1211
- const output = stderr ? `${stdout}
1212
- ${stderr}`.trim() : stdout.trim();
1213
- const exitCode = proc.exitCode ?? (proc.signalCode ? 1 : 1);
1214
- return { ok: exitCode === 0, exitCode, output, durationMs };
1215
648
  }
1216
-
1217
- class WorkflowsDaemon {
1218
- store;
1219
- wal;
1220
- worker;
1221
- leaseTtlMs;
1222
- maxAttempts;
1223
- maxDispatchPerCycle;
1224
- maxStepsPerCycle;
1225
- time;
1226
- laneRunner;
1227
- env;
1228
- leases = new Map;
1229
- fencingCounter = new Map;
1230
- graphCache = new Map;
1231
- isolatedRunId = null;
1232
- constructor(store, wal, options = {}) {
1233
- this.store = store;
1234
- this.wal = wal;
1235
- this.worker = options.worker ?? "local";
1236
- this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
1237
- this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS2;
1238
- this.maxDispatchPerCycle = options.maxDispatchPerCycle ?? DEFAULT_MAX_DISPATCH;
1239
- this.maxStepsPerCycle = options.maxStepsPerCycle ?? DEFAULT_MAX_STEPS;
1240
- this.time = options.time ?? Date.now;
1241
- this.env = options.env ?? {};
1242
- this.laneRunner = options.laneRunner ?? defaultLaneRunner;
1243
- this.isolatedRunId = options.isolatedRunId ?? null;
1244
- this.replayRegisteredGraphs();
1245
- }
1246
- isolateRun(runId) {
1247
- this.isolatedRunId = runId;
1248
- }
1249
- replayRegisteredGraphs() {
1250
- const entries = this.wal.replay().entries;
1251
- for (const entry of entries) {
1252
- const op = entry.op;
1253
- if (op.op === "graph_registered") {
1254
- this.graphCache.set(op.name, op.graph);
1255
- }
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}`);
1256
667
  }
668
+ return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
1257
669
  }
1258
- liveLeases() {
1259
- const now = this.time();
1260
- for (const [runId, lease] of this.leases) {
1261
- if (lease.expiresAtMs <= now)
1262
- this.leases.delete(runId);
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
+ };
684
+ }
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";
1263
692
  }
1264
- return this.leases;
1265
- }
1266
- claim(runId) {
1267
- const now = this.time();
1268
- const existing = this.leases.get(runId);
1269
- if (existing && existing.worker !== this.worker && existing.expiresAtMs > now) {
1270
- return null;
693
+ };
694
+ LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
695
+ constructor(message) {
696
+ super(message);
697
+ this.name = "LaneAdapterShapeError";
1271
698
  }
1272
- const liveClaims = this.wal.replay().liveClaims(now);
1273
- const foreign = liveClaims.get(runId);
1274
- if (foreign && foreign.worker !== this.worker && foreign.expiresAtMs > now) {
1275
- return null;
699
+ };
700
+ LaneTimeoutError = class LaneTimeoutError extends Error {
701
+ constructor(what, timeoutMs) {
702
+ super(`${what} exceeded the ${timeoutMs}ms budget`);
703
+ this.name = "LaneTimeoutError";
1276
704
  }
1277
- const fencing = (this.fencingCounter.get(runId) ?? 0) + 1;
1278
- this.fencingCounter.set(runId, fencing);
1279
- const lease = { runId, worker: this.worker, expiresAtMs: now + this.leaseTtlMs, fencing };
1280
- this.leases.set(runId, lease);
1281
- this.wal.append({
1282
- op: "claim_acquired",
1283
- runId,
1284
- worker: this.worker,
1285
- expiresAt: new Date(lease.expiresAtMs).toISOString(),
1286
- fencing,
1287
- at: nowIso()
1288
- });
1289
- return lease;
1290
- }
1291
- heartbeat(runId, fencing) {
1292
- const lease = this.leases.get(runId);
1293
- if (!lease || lease.fencing !== fencing)
1294
- return false;
1295
- lease.expiresAtMs = this.time() + this.leaseTtlMs;
1296
- return true;
1297
- }
1298
- release(runId, fencing) {
1299
- const lease = this.leases.get(runId);
1300
- if (!lease || lease.fencing !== fencing)
1301
- return false;
1302
- this.leases.delete(runId);
1303
- this.wal.append({ op: "claim_released", runId, fencing, at: nowIso() });
1304
- return true;
1305
- }
1306
- async reap() {
1307
- const report = { expired: 0, interrupted: 0, requeued: 0, failed: 0, dispatched: 0, advanced: 0, completed: 0 };
1308
- const now = this.time();
1309
- for (const [runId, lease] of this.leases) {
1310
- if (lease.expiresAtMs <= now) {
1311
- this.leases.delete(runId);
1312
- report.expired++;
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) };
1313
777
  }
1314
778
  }
1315
- if (this.isolatedRunId === null) {
1316
- const repair = repairTornRuns(this.store, this.wal, { maxAttempts: this.maxAttempts, now });
1317
- report.interrupted += repair.interrupted;
1318
- report.requeued += repair.requeued;
1319
- report.failed += repair.failed;
1320
- }
1321
- const pending = this.store.listRuns({ status: "pending", limit: this.maxDispatchPerCycle }).concat(this.store.listRuns({ status: "interrupted", limit: this.maxDispatchPerCycle })).sort((a, b) => a.createdAt === b.createdAt ? a.id < b.id ? 1 : -1 : a.createdAt < b.createdAt ? 1 : -1).slice(0, this.maxDispatchPerCycle);
1322
- for (const run of pending) {
1323
- if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
1324
- continue;
1325
- const lease = this.claim(run.id);
1326
- if (!lease)
1327
- continue;
1328
- this.store.setRunStatus(run.id, "running");
1329
- this.wal.append({ op: "run_started", runId: run.id, at: nowIso() });
1330
- report.dispatched++;
1331
- }
1332
- const running = this.store.listRuns({ status: "running", limit: this.maxStepsPerCycle });
1333
- let steps = 0;
1334
- for (const run of running) {
1335
- if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
1336
- continue;
1337
- if (steps >= this.maxStepsPerCycle)
1338
- break;
1339
- const outcome = await this.advanceRun(run.id);
1340
- steps++;
1341
- if (outcome.kind === "completed")
1342
- report.completed++;
1343
- if (outcome.kind === "failed")
1344
- report.failed++;
1345
- if (outcome.kind === "advanced")
1346
- report.advanced++;
1347
- }
1348
- return report;
1349
- }
1350
- startRun(graph, context) {
1351
- const validation = validateGraph(graph);
1352
- if (!validation.ok) {
1353
- throw new Error(`graph validation failed: ${validation.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
1354
- }
1355
- assertNoSecrets(context, "run context");
1356
- assertNoSecrets(graph, "graph definition");
1357
- const existing = this.graphCache.get(graph.name);
1358
- const serialized = JSON.stringify(graph);
1359
- if (!existing || JSON.stringify(existing) !== serialized) {
1360
- this.graphCache.set(graph.name, graph);
1361
- this.wal.append({ op: "graph_registered", name: graph.name, version: graph.version, graph, at: nowIso() });
1362
- } else {
1363
- this.graphCache.set(graph.name, existing);
779
+ };
780
+ }
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 };
1364
794
  }
1365
- return this.store.createRun({ graphName: graph.name, graphVersion: graph.version, context: context ?? {} });
795
+ throw err;
1366
796
  }
1367
- async advanceRun(runId) {
1368
- const run = this.store.getRun(runId);
1369
- if (!run || run.status !== "running")
1370
- return { kind: "noop" };
1371
- const lease = this.leases.get(runId);
1372
- if (!lease || lease.expiresAtMs <= this.time())
1373
- return { kind: "blocked" };
1374
- const graph = this.loadGraph(run.graphName);
1375
- if (!graph) {
1376
- this.failRun(run, `graph ${run.graphName} not found in daemon graph cache`);
1377
- return { kind: "failed", error: "graph not found" };
1378
- }
1379
- const context = JSON.parse(run.contextJson);
1380
- const rawWf = context.__wf ?? {};
1381
- const wf = {
1382
- ...rawWf,
1383
- cursor: typeof rawWf.cursor === "string" ? rawWf.cursor : undefined,
1384
- loops: rawWf.loops ?? {},
1385
- completedLoops: rawWf.completedLoops ?? {}
1386
- };
1387
- try {
1388
- const cursor = wf.cursor;
1389
- const node = cursor === undefined ? graph.nodes.find((n) => n.type === "start") : findNode(graph, cursor);
1390
- if (!node) {
1391
- this.failRun(run, `cursor ${cursor ?? "<start>"} resolves to no node`);
1392
- return { kind: "failed", error: "no node at cursor" };
1393
- }
1394
- switch (node.type) {
1395
- case "start": {
1396
- wf.cursor = node.next;
1397
- this.persistContext(run, context, wf);
1398
- return { kind: "advanced", nodeId: node.id };
1399
- }
1400
- case "step": {
1401
- return await this.executeStep(run, graph, context, wf, node);
1402
- }
1403
- case "decision": {
1404
- const value = evaluateExpr(node.condition, this.conditionScope(context));
1405
- const next = value ? node.then : node.else;
1406
- if (!next) {
1407
- this.failRun(run, `decision ${node.id} evaluated ${value} but no ${value ? "then" : "else"} edge`);
1408
- return { kind: "failed", error: "missing decision edge" };
1409
- }
1410
- wf.cursor = next;
1411
- this.persistContext(run, context, wf);
1412
- return { kind: "advanced", nodeId: node.id };
797
+ }
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" };
1413
814
  }
1414
- case "while": {
1415
- return this.executeWhile(run, graph, context, wf, node);
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()");
1416
856
  }
1417
- case "end": {
1418
- this.completeRun(run, context);
1419
- return { kind: "completed" };
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);
1420
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) };
1421
867
  }
1422
- } catch (err) {
1423
- if (err instanceof ExprEvalError || err instanceof ExprSyntaxError) {
1424
- this.failRun(run, `condition error at ${wf.cursor ?? "<start>"}: ${err.message}`);
1425
- return { kind: "failed", error: err.message };
1426
- }
1427
- throw err;
1428
868
  }
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 };
887
+ }
888
+ throw err;
1429
889
  }
1430
- loadGraph(name) {
1431
- return this.graphCache.get(name);
1432
- }
1433
- conditionScope(context) {
1434
- return { ...context, steps: context.steps ?? {} };
1435
- }
1436
- persistContext(run, context, wf) {
1437
- context.__wf = wf;
1438
- this.store.setRunContext(run.id, context);
1439
- }
1440
- async executeStep(run, graph, context, wf, node) {
1441
- const nodeId = node.id;
1442
- const latest = this.store.getLatestRunNode(run.id, nodeId);
1443
- const isLoopBody = graph.nodes.some((candidate) => candidate.type === "while" && candidate.body.includes(nodeId));
1444
- const nodeRow = latest === undefined || isLoopBody && latest.status === "completed" ? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" }) : latest;
1445
- const memoWatch = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
1446
- const memoSource = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
1447
- const memoInput = node.memo ? memoWatch !== undefined ? { ...memoSource, memoWatch } : memoSource : undefined;
1448
- if (memoInput !== undefined) {
1449
- const hit = tryMemoHit(this.store, run.graphName, nodeId, memoInput);
1450
- if (hit) {
1451
- const cached = JSON.parse(hit.outputJson);
1452
- this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: Number(cached.exitCode ?? 0), output: { ...cached, memoHit: true } });
1453
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
1454
- this.recordStepResult(context, nodeId, { ...cached, memoHit: true });
1455
- return this.afterNode(run, context, wf, node.next);
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
+ };
1456
918
  }
1457
- }
1458
- if (nodeRow.status !== "running") {
1459
- this.store.setRunNodeStatus(nodeRow.id, "running");
1460
- this.wal.append({ op: "node_started", runId: run.id, nodeId, at: nowIso() });
1461
- }
1462
- const job = {
1463
- lane: node.lane ?? "claude",
1464
- prompt: node.prompt,
1465
- command: node.command,
1466
- timeoutMs: node.timeoutMs,
1467
- env: this.env
1468
- };
1469
- let result;
1470
- try {
1471
- result = await this.laneRunner(job);
1472
- } catch (err) {
1473
- result = { ok: false, exitCode: 1, output: "", durationMs: 0, error: String(err instanceof Error ? err.message : err) };
1474
- }
1475
- const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1476
- if (!result.ok) {
1477
- const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1478
- this.store.bumpAttemptsNode(nodeRow.id);
1479
- if (shouldRetry) {
1480
- this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1481
- return { kind: "advanced", nodeId };
919
+ },
920
+ async run(job) {
921
+ if (!job.prompt) {
922
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
1482
923
  }
1483
- const error = result.error ?? `step ${nodeId} failed with exit ${result.exitCode}`;
1484
- this.store.setRunNodeStatus(nodeRow.id, "failed", { exitCode: result.exitCode, error });
1485
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
1486
- this.failRun(run, error);
1487
- return { kind: "failed", error };
1488
- }
1489
- try {
1490
- assertNoSecrets(output, `node ${nodeId} output`);
1491
- } catch (err) {
1492
- const message = err instanceof Error ? err.message : String(err);
1493
- this.store.setRunNodeStatus(nodeRow.id, "failed", { error: message });
1494
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
1495
- this.failRun(run, message);
1496
- return { kind: "failed", error: message };
1497
- }
1498
- this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: result.exitCode, output });
1499
- this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
1500
- if (memoInput !== undefined) {
1501
- const memoInputAfter = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
1502
- const stable = memoWatch === undefined && memoInputAfter === undefined || memoWatch !== undefined && memoInputAfter !== undefined && JSON.stringify(memoWatch) === JSON.stringify(memoInputAfter);
1503
- if (stable) {
1504
- recordMemo(this.store, run.graphName, nodeId, memoInput, output);
1505
- this.wal.append({ op: "memo_set", key: memoKey(run.graphName, nodeId, memoInput), at: nowIso() });
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);
1506
934
  }
1507
- }
1508
- this.recordStepResult(context, nodeId, output);
1509
- return this.afterNode(run, context, wf, node.next);
1510
- }
1511
- afterNode(run, context, wf, next) {
1512
- if (next) {
1513
- wf.cursor = next;
1514
- this.persistContext(run, context, wf);
1515
- return { kind: "advanced", nodeId: wf.cursor };
1516
- }
1517
- const cursor = wf.cursor;
1518
- const graph = this.loadGraph(run.graphName);
1519
- const whileNode = graph.nodes.find((n) => n.type === "while" && n.body.includes(cursor ?? ""));
1520
- if (whileNode) {
1521
- const index = whileNode.body.indexOf(cursor ?? "");
1522
- if (index >= 0 && index < whileNode.body.length - 1) {
1523
- wf.cursor = whileNode.body[index + 1];
1524
- this.persistContext(run, context, wf);
1525
- return { kind: "advanced", nodeId: wf.cursor };
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) };
1526
952
  }
1527
- wf.loops[whileNode.id] = (wf.loops[whileNode.id] ?? 0) + 1;
1528
- wf.cursor = whileNode.id;
1529
- this.persistContext(run, context, wf);
1530
- return { kind: "advanced", nodeId: whileNode.id };
1531
953
  }
1532
- this.completeRun(run, context);
1533
- return { kind: "completed" };
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");
1534
961
  }
1535
- executeWhile(run, graph, context, wf, node) {
1536
- const iteration = wf.loops[node.id] ?? 0;
1537
- if (iteration >= node.maxIterations) {
1538
- const error = `while node ${node.id} exceeded maxIterations (${node.maxIterations})`;
1539
- this.failRun(run, error);
1540
- return { kind: "failed", error };
1541
- }
1542
- const scope = { ...this.conditionScope(context), i: iteration };
1543
- const keepGoing = evaluateExpr(node.condition, scope);
1544
- if (!keepGoing) {
1545
- wf.completedLoops[node.id] = iteration;
1546
- delete wf.loops[node.id];
1547
- wf.cursor = node.next;
1548
- this.persistContext(run, context, wf);
1549
- return { kind: "advanced", nodeId: node.id };
1550
- }
1551
- wf.cursor = node.body[0];
1552
- this.persistContext(run, context, wf);
1553
- return { kind: "advanced", nodeId: node.id };
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()");
1554
966
  }
1555
- recordStepResult(context, nodeId, output) {
1556
- const steps = context.steps ?? {};
1557
- steps[nodeId] = output;
1558
- context.steps = steps;
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();
1559
976
  }
1560
- completeRun(run, context) {
1561
- const result = {
1562
- steps: context.steps ?? {},
1563
- iterations: context.__wf?.completedLoops ?? {}
1564
- };
1565
- try {
1566
- assertNoSecrets(result, "run result");
1567
- } catch (err) {
1568
- const message = err instanceof Error ? err.message : String(err);
1569
- this.failRun(run, message);
1570
- return;
1571
- }
1572
- this.store.setRunStatus(run.id, "completed", { result, finishedAt: nowIso(), error: null });
1573
- this.wal.append({ op: "run_finished", runId: run.id, status: "completed", at: nowIso() });
1574
- const lease = this.leases.get(run.id);
1575
- if (lease)
1576
- this.release(run.id, lease.fencing);
977
+ if (!run || typeof run !== "object") {
978
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
1577
979
  }
1578
- failRun(run, error) {
1579
- this.store.setRunStatus(run.id, "failed", { error, finishedAt: nowIso() });
1580
- this.wal.append({ op: "run_finished", runId: run.id, status: "failed", at: nowIso() });
1581
- const lease = this.leases.get(run.id);
1582
- if (lease)
1583
- this.release(run.id, lease.fencing);
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)`);
1584
983
  }
1585
- }
1586
- var defaultLaneRunner = async (job) => {
1587
- if (job.command)
1588
- return runCommand(job);
1589
- const { runLaneJob: runLaneJob2 } = await Promise.resolve().then(() => (init_lanes(), exports_lanes));
1590
- return runLaneJob2(job);
1591
- };
1592
- async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1593
- const daemon = new WorkflowsDaemon(store, wal, {
1594
- worker: "local",
1595
- laneRunner: options.laneRunner,
1596
- time: options.time,
1597
- env: options.env,
1598
- isolatedRunId: options.isolatedRunId
1599
- });
1600
- const run = daemon.startRun(graph, context);
1601
- options.onRunCreated?.(run);
1602
- daemon.isolateRun(run.id);
1603
- const maxCycles = options.maxCycles ?? 500;
1604
- for (let cycle = 0;cycle < maxCycles; cycle++) {
1605
- const report = await daemon.reap();
1606
- const current = store.getRun(run.id);
1607
- if (current.status !== "running")
1608
- return current;
1609
- if (report.advanced === 0 && report.dispatched === 0) {
1610
- return store.getRun(run.id);
1611
- }
984
+ if (typeof candidate.result !== "string") {
985
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
1612
986
  }
1613
- return store.getRun(run.id);
1614
- }
1615
-
1616
- // src/agent-tools.ts
1617
- init_lanes();
1618
-
1619
- // src/store.ts
1620
- import { Database } from "bun:sqlite";
1621
- import { mkdirSync } from "fs";
1622
- import { join as join2 } from "path";
1623
- import { randomUUID } from "crypto";
1624
- function nowIso2() {
1625
- return new Date().toISOString();
1626
- }
1627
- var BUSY_TIMEOUT_MS = 3000;
1628
- var BUSY_RETRIES = 2;
1629
- var BUSY_RETRY_SLEEP_MS = 50;
1630
- function isBusyError(err) {
1631
- if (!(err instanceof Error))
1632
- return false;
1633
- const code = String(err.code ?? "");
1634
- return code.includes("BUSY") || /\bdatabase is locked\b/i.test(err.message);
987
+ return { status: candidate.status, result: candidate.result };
1635
988
  }
1636
- function retryBusy(fn) {
1637
- let last;
1638
- for (let attempt = 0;attempt <= BUSY_RETRIES; attempt++) {
1639
- try {
1640
- return fn();
1641
- } catch (err) {
1642
- if (!isBusyError(err))
1643
- throw err;
1644
- last = err;
1645
- Bun.sleepSync(BUSY_RETRY_SLEEP_MS);
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 };
1646
1002
  }
1003
+ throw err;
1647
1004
  }
1648
- throw last;
1649
1005
  }
1650
- function openStore(dataDir) {
1651
- mkdirSync(dataDir, { recursive: true });
1652
- const db = new Database(join2(dataDir, "workflows.db"));
1653
- retryBusy(() => db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`));
1654
- retryBusy(() => db.exec("PRAGMA journal_mode = WAL"));
1655
- retryBusy(() => db.exec("PRAGMA foreign_keys = ON"));
1656
- retryBusy(() => db.exec(`
1657
- CREATE TABLE IF NOT EXISTS runs (
1658
- id TEXT PRIMARY KEY,
1659
- graph_name TEXT NOT NULL,
1660
- graph_version TEXT NOT NULL,
1661
- status TEXT NOT NULL,
1662
- attempts INTEGER NOT NULL DEFAULT 0,
1663
- context_json TEXT NOT NULL,
1664
- result_json TEXT,
1665
- error TEXT,
1666
- started_at TEXT,
1667
- finished_at TEXT,
1668
- created_at TEXT NOT NULL,
1669
- updated_at TEXT NOT NULL
1670
- );
1671
- CREATE TABLE IF NOT EXISTS run_nodes (
1672
- id TEXT PRIMARY KEY,
1673
- run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
1674
- node_id TEXT NOT NULL,
1675
- status TEXT NOT NULL,
1676
- attempts INTEGER NOT NULL DEFAULT 0,
1677
- lane TEXT,
1678
- exit_code INTEGER,
1679
- output_json TEXT,
1680
- error TEXT,
1681
- started_at TEXT,
1682
- finished_at TEXT
1683
- );
1684
- CREATE TABLE IF NOT EXISTS memos (
1685
- key TEXT PRIMARY KEY,
1686
- graph_name TEXT NOT NULL,
1687
- node_id TEXT NOT NULL,
1688
- input_hash TEXT NOT NULL,
1689
- output_json TEXT NOT NULL,
1690
- hit_count INTEGER NOT NULL DEFAULT 0,
1691
- created_at TEXT NOT NULL
1692
- );
1693
- CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
1694
- CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
1695
- CREATE INDEX IF NOT EXISTS idx_run_nodes_run ON run_nodes(run_id);
1696
- `));
1697
- const rowToRun = (r) => ({
1698
- id: String(r.id),
1699
- graphName: String(r.graph_name),
1700
- graphVersion: String(r.graph_version),
1701
- status: r.status,
1702
- attempts: Number(r.attempts),
1703
- contextJson: String(r.context_json),
1704
- resultJson: r.result_json === null ? null : String(r.result_json),
1705
- error: r.error === null ? null : String(r.error),
1706
- startedAt: r.started_at === null ? null : String(r.started_at),
1707
- finishedAt: r.finished_at === null ? null : String(r.finished_at),
1708
- createdAt: String(r.created_at),
1709
- updatedAt: String(r.updated_at)
1710
- });
1711
- const rowToNode = (r) => ({
1712
- id: String(r.id),
1713
- runId: String(r.run_id),
1714
- nodeId: String(r.node_id),
1715
- status: r.status,
1716
- attempts: Number(r.attempts),
1717
- lane: r.lane === null ? null : String(r.lane),
1718
- exitCode: r.exit_code === null ? null : Number(r.exit_code),
1719
- outputJson: r.output_json === null ? null : String(r.output_json),
1720
- error: r.error === null ? null : String(r.error),
1721
- startedAt: r.started_at === null ? null : String(r.started_at),
1722
- finishedAt: r.finished_at === null ? null : String(r.finished_at)
1723
- });
1724
- const rowToMemo = (r) => ({
1725
- key: String(r.key),
1726
- graphName: String(r.graph_name),
1727
- nodeId: String(r.node_id),
1728
- inputHash: String(r.input_hash),
1729
- outputJson: String(r.output_json),
1730
- hitCount: Number(r.hit_count),
1731
- createdAt: String(r.created_at)
1732
- });
1006
+ var init_cursor = __esm(() => {
1007
+ init_types();
1008
+ });
1009
+
1010
+ // src/lanes/grok.ts
1011
+ function createGrokAdapter(deps = {}) {
1733
1012
  return {
1734
- dataDir,
1735
- close() {
1736
- db.close();
1737
- },
1738
- listTables() {
1739
- const rows = retryBusy(() => db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all());
1740
- return rows.map((r) => r.name);
1741
- },
1742
- createRun(input) {
1743
- const id = randomUUID();
1744
- const now = nowIso2();
1745
- retryBusy(() => db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
1746
- VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now));
1747
- return this.getRun(id);
1748
- },
1749
- getRun(id) {
1750
- const row = retryBusy(() => db.query(`SELECT * FROM runs WHERE id = ?`).get(id));
1751
- return row ? rowToRun(row) : undefined;
1752
- },
1753
- listRuns(opts = {}) {
1754
- const limit = Math.min(opts.limit ?? 100, 1000);
1755
- if (opts.status) {
1756
- const status = opts.status;
1757
- const rows2 = retryBusy(() => db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(status, limit));
1758
- return rows2.map((r) => rowToRun(r));
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" };
1759
1018
  }
1760
- const rows = retryBusy(() => db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit));
1761
- return rows.map((r) => rowToRun(r));
1762
- },
1763
- setRunStatus(id, status, patch = {}) {
1764
- const now = nowIso2();
1765
- const existing = this.getRun(id);
1766
- const resultJson = patch.result !== undefined ? JSON.stringify(patch.result) : existing?.resultJson ?? null;
1767
- const error = patch.error !== undefined ? patch.error : existing?.error ?? null;
1768
- const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing?.startedAt === null ? now : existing?.startedAt ?? null;
1769
- const terminal = status === "completed" || status === "failed" || status === "cancelled" || status === "interrupted";
1770
- const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing?.finishedAt === null ? now : existing?.finishedAt ?? null;
1771
- retryBusy(() => db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id));
1772
- },
1773
- bumpAttempts(id) {
1774
- retryBusy(() => db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id));
1775
- },
1776
- setRunContext(id, context) {
1777
- retryBusy(() => db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id));
1778
- },
1779
- createRunNode(input) {
1780
- const id = randomUUID();
1781
- retryBusy(() => db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
1782
- VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null));
1783
- return this.getRunNode(id);
1784
- },
1785
- getRunNode(id) {
1786
- const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id));
1787
- return row ? rowToNode(row) : undefined;
1788
- },
1789
- listRunNodes(runId) {
1790
- const rows = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId));
1791
- return rows.map((r) => rowToNode(r));
1792
- },
1793
- getLatestRunNode(runId, nodeId) {
1794
- const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? AND node_id = ? ORDER BY rowid DESC LIMIT 1`).get(runId, nodeId));
1795
- return row ? rowToNode(row) : undefined;
1796
- },
1797
- setRunNodeStatus(id, status, patch = {}) {
1798
- const now = nowIso2();
1799
- const existing = this.getRunNode(id);
1800
- if (!existing)
1801
- return;
1802
- const exitCode = patch.exitCode !== undefined ? patch.exitCode : existing.exitCode;
1803
- const outputJson = patch.output !== undefined ? JSON.stringify(patch.output) : existing.outputJson;
1804
- const error = patch.error !== undefined ? patch.error : existing.error;
1805
- const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing.startedAt === null ? now : existing.startedAt;
1806
- const terminal = status === "completed" || status === "failed" || status === "skipped";
1807
- const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing.finishedAt === null ? now : existing.finishedAt;
1808
- retryBusy(() => db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id));
1809
- },
1810
- bumpAttemptsNode(id) {
1811
- retryBusy(() => db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id));
1812
- },
1813
- memoPut(key, graphName, nodeId, inputHash2, outputJson) {
1814
- retryBusy(() => db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
1815
- VALUES (?, ?, ?, ?, ?, 0, ?)
1816
- ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2()));
1817
- },
1818
- memoGet(key) {
1819
- const row = retryBusy(() => db.query(`SELECT * FROM memos WHERE key = ?`).get(key));
1820
- return row ? rowToMemo(row) : undefined;
1821
- },
1822
- memoHit(key) {
1823
- retryBusy(() => db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key));
1824
- },
1825
- memoList() {
1826
- const rows = retryBusy(() => db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all());
1827
- return rows.map((r) => rowToMemo(r));
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
+ };
1828
1025
  },
1829
- memoClear() {
1830
- retryBusy(() => db.exec(`DELETE FROM memos`));
1026
+ async run(job) {
1027
+ if (!job.prompt) {
1028
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
1029
+ }
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 };
1045
+ }
1046
+ throw err;
1047
+ }
1831
1048
  }
1832
1049
  };
1833
1050
  }
1051
+ var init_grok = __esm(() => {
1052
+ init_types();
1053
+ });
1834
1054
 
1835
- // src/wal.ts
1836
- import { createHash as createHash2 } from "crypto";
1837
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, truncateSync, writeFileSync } from "fs";
1838
- import { join as join3 } from "path";
1839
- var WAL_FILE_NAME = "session.wal";
1840
- var SESSIONS_DIR_NAME = "sessions";
1841
- var MAX_WAL_BYTES = 256 * 1024 * 1024;
1842
- function checksumOf(op) {
1843
- if (op === undefined || op === null)
1844
- return "";
1845
- return createHash2("sha256").update(JSON.stringify(op)).digest("hex");
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;
1846
1076
  }
1847
-
1848
- class SessionWAL {
1849
- filePath;
1850
- constructor(filePath) {
1851
- this.filePath = filePath;
1852
- }
1853
- static open(dataDir) {
1854
- mkdirSync2(join3(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1855
- const canonical = join3(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1856
- const legacy = join3(dataDir, WAL_FILE_NAME);
1857
- if (existsSync(legacy) && !existsSync(canonical)) {
1858
- try {
1859
- renameSync(legacy, canonical);
1860
- } catch {
1861
- return new SessionWAL(legacy);
1862
- }
1863
- }
1864
- return new SessionWAL(canonical);
1865
- }
1866
- static openAt(filePath) {
1867
- return new SessionWAL(filePath);
1868
- }
1869
- append(op) {
1870
- const at = op.at ?? new Date().toISOString();
1871
- const full = { ...op, at };
1872
- const nextSeq = this.nextSeq();
1873
- const entry = { seq: nextSeq, at, checksum: checksumOf(full), op: full };
1874
- appendFileSync(this.filePath, JSON.stringify(entry) + `
1875
- `, "utf8");
1876
- return entry;
1877
- }
1878
- nextSeq() {
1879
- const replay = this.replay();
1880
- return replay.entries.length + 1;
1881
- }
1882
- replay() {
1883
- const entries = [];
1884
- let torn = false;
1885
- let repaired = false;
1886
- let content;
1887
- try {
1888
- content = readFileSync2(this.filePath, "utf8");
1889
- } catch {
1890
- return { entries, torn, repaired, liveClaims: () => new Map };
1891
- }
1892
- if (content.length > MAX_WAL_BYTES) {
1893
- throw new Error(`session WAL exceeds ${MAX_WAL_BYTES} bytes; refusing to replay a truncated journal`);
1894
- }
1895
- if (content === "") {
1896
- return { entries, torn, repaired, liveClaims: () => new Map };
1897
- }
1898
- const lines = content.split(`
1899
- `);
1900
- let badOffset = null;
1901
- let expectedSeq = 1;
1902
- for (let i = 0;i < lines.length; i++) {
1903
- const line = lines[i];
1904
- if (line === "")
1905
- continue;
1906
- if (badOffset !== null)
1907
- break;
1908
- const lineOffset = cumulativeOffset(lines, i);
1909
- let entry;
1910
- try {
1911
- entry = JSON.parse(line);
1912
- } catch {
1913
- badOffset = lineOffset;
1914
- break;
1915
- }
1916
- if (typeof entry?.seq !== "number" || entry.seq !== expectedSeq || entry.checksum !== checksumOf(entry.op)) {
1917
- badOffset = lineOffset;
1918
- break;
1919
- }
1920
- entries.push(entry);
1921
- expectedSeq++;
1922
- }
1923
- if (badOffset !== null) {
1924
- torn = true;
1925
- try {
1926
- truncateSync(this.filePath, badOffset);
1927
- repaired = true;
1928
- } catch {}
1929
- }
1930
- return {
1931
- entries,
1932
- torn,
1933
- repaired,
1934
- liveClaims: (nowMs) => reconstructLiveClaims(entries, nowMs)
1935
- };
1936
- }
1937
- close() {}
1077
+ async function runLaneJob(job) {
1078
+ return resolveLane(job.lane).run(job);
1938
1079
  }
1939
- function cumulativeOffset(lines, index) {
1940
- let offset = 0;
1941
- for (let i = 0;i < index; i++) {
1942
- offset += Buffer.byteLength(lines[i]) + 1;
1943
- }
1944
- return offset;
1080
+ async function probeLane(kind) {
1081
+ return resolveLane(kind).probe();
1945
1082
  }
1946
- function reconstructLiveClaims(entries, nowMs = Date.now()) {
1947
- const claims = new Map;
1948
- for (const entry of entries) {
1949
- const op = entry.op;
1950
- if (op.op === "claim_acquired") {
1951
- claims.set(op.runId, { worker: op.worker, fencing: op.fencing, expiresAtMs: Date.parse(op.expiresAt) });
1952
- } else if (op.op === "claim_released") {
1953
- const existing = claims.get(op.runId);
1954
- if (existing && existing.fencing === op.fencing) {
1955
- claims.delete(op.runId);
1956
- }
1957
- }
1958
- }
1959
- for (const [runId, claim] of claims) {
1960
- if (claim.expiresAtMs <= nowMs)
1961
- claims.delete(runId);
1962
- }
1963
- return claims;
1083
+ async function laneInventory() {
1084
+ return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
1964
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
+ });
1965
1105
 
1966
1106
  // src/agent-tools.ts
1107
+ init_graph();
1108
+ init_lanes();
1109
+
1110
+ // src/tool-definitions.ts
1967
1111
  function textResult(payload) {
1968
1112
  return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: false };
1969
1113
  }
1970
1114
  function workflowsTools(_service) {
1971
- return [
1115
+ const tools = [
1972
1116
  {
1973
1117
  name: "workflows_version",
1974
1118
  description: "Report the installed @hasna/workflows version.",
@@ -1981,7 +1125,7 @@ function workflowsTools(_service) {
1981
1125
  },
1982
1126
  {
1983
1127
  name: "workflows_ready",
1984
- description: "Report the workflows service readiness (version check).",
1128
+ description: "Report the hosted workflows service readiness.",
1985
1129
  inputSchema: { type: "object", properties: {} }
1986
1130
  },
1987
1131
  {
@@ -2000,14 +1144,16 @@ function workflowsTools(_service) {
2000
1144
  type: "object",
2001
1145
  properties: {
2002
1146
  graph: { type: "object", description: "the workflow graph" },
2003
- context: { type: "object", description: "optional run context" }
1147
+ context: { type: "object", description: "optional run context" },
1148
+ idempotencyKey: { type: "string", description: "stable request key" },
1149
+ maxCycles: { type: "integer", minimum: 1, maximum: 2000 }
2004
1150
  },
2005
- required: ["graph"]
1151
+ required: ["graph", "idempotencyKey"]
2006
1152
  }
2007
1153
  },
2008
1154
  {
2009
1155
  name: "workflows_runs_list",
2010
- description: "List recent runs from the local store.",
1156
+ description: "List recent runs from the configured hosted service.",
2011
1157
  inputSchema: {
2012
1158
  type: "object",
2013
1159
  properties: {
@@ -2022,7 +1168,13 @@ function workflowsTools(_service) {
2022
1168
  inputSchema: { type: "object", properties: {} }
2023
1169
  }
2024
1170
  ];
1171
+ return tools.map((tool) => ({ ...tool, inputSchema: { ...tool.inputSchema, properties: {
1172
+ ...tool.inputSchema.properties,
1173
+ agent: { type: "string", description: "calling agent identity" }
1174
+ } } }));
2025
1175
  }
1176
+
1177
+ // src/agent-tools.ts
2026
1178
  async function callWorkflowTool(service, name, args) {
2027
1179
  switch (name) {
2028
1180
  case "workflows_version":
@@ -2030,42 +1182,26 @@ async function callWorkflowTool(service, name, args) {
2030
1182
  case "workflows_health":
2031
1183
  return textResult(service.health());
2032
1184
  case "workflows_ready":
2033
- return textResult(service.ready());
1185
+ return textResult(await service.ready());
2034
1186
  case "workflows_validate": {
2035
1187
  const graph = args.graph;
2036
1188
  const result = validateGraph(graph);
2037
1189
  return textResult(result);
2038
1190
  }
2039
- case "workflows_run": {
2040
- const graph = args.graph;
2041
- const store = openStore(service.config.dataDir);
2042
- const wal = SessionWAL.open(service.config.dataDir);
2043
- try {
2044
- const final = await runGraphToCompletion(store, wal, graph, args.context ?? {});
2045
- return textResult({
2046
- runId: final.id,
2047
- status: final.status,
2048
- error: final.error ?? null,
2049
- result: final.resultJson ? JSON.parse(final.resultJson) : null
2050
- });
2051
- } finally {
2052
- store.close();
2053
- }
2054
- }
2055
- case "workflows_runs_list": {
2056
- const store = openStore(service.config.dataDir);
2057
- try {
2058
- const status = typeof args.status === "string" ? args.status : undefined;
2059
- const limit = typeof args.limit === "number" ? args.limit : 100;
2060
- return textResult(store.listRuns({ status, limit }));
2061
- } finally {
2062
- store.close();
2063
- }
2064
- }
1191
+ case "workflows_run":
1192
+ return textResult(await service.triggerRun(args.graph, args.context ?? {}, {
1193
+ idempotencyKey: typeof args.idempotencyKey === "string" ? args.idempotencyKey : undefined,
1194
+ maxCycles: typeof args.maxCycles === "number" ? args.maxCycles : undefined
1195
+ }));
1196
+ case "workflows_runs_list":
1197
+ return textResult(await service.listRuns({
1198
+ status: typeof args.status === "string" ? args.status : undefined,
1199
+ limit: typeof args.limit === "number" ? args.limit : 100
1200
+ }));
2065
1201
  case "workflows_lanes_list":
2066
1202
  return textResult(await laneInventory());
2067
1203
  default:
2068
- throw new Error(`Unknown tool: ${name}`);
1204
+ throw new Error("Unknown workflows tool");
2069
1205
  }
2070
1206
  }
2071
1207
  export {