@hasna/workflows 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/handlers.js CHANGED
@@ -16,1154 +16,1196 @@ 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 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 run2 = await withTimeout(runLocalAgent(sdk, job, (callback) => {
335
- cancel = callback;
336
- }), timeoutMs, "cursor lane", () => cancel?.());
337
- const durationMs = Date.now() - started;
338
- const status = run2.status;
339
- const ok = status === "finished";
340
- return { ok, exitCode: ok ? 0 : 1, output: run2.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 run2 = await agent.send(job.prompt, { model: job.model });
362
- if (run2 && typeof run2 === "object") {
363
- const handle = run2;
364
- if (typeof handle.cancel === "function")
365
- onCancel(() => {
366
- handle.cancel().catch(() => {});
367
- });
368
- if (typeof handle.wait === "function")
369
- run2 = await handle.wait();
100
+ next() {
101
+ return this.tokens[this.pos++];
370
102
  }
371
- if (!run2 || typeof run2 !== "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 = run2;
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
- };
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
+ }
444
262
  }
445
- var init_grok = __esm(() => {
446
- init_types();
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;
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(["==", "!=", "<", "<=", ">", ">="]);
447
286
  });
448
287
 
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);
288
+ // src/graph.ts
289
+ function issue(issues, path, message) {
290
+ issues.push({ path, message });
473
291
  }
474
- async function probeLane(kind) {
475
- return resolveLane(kind).probe();
292
+ function findNode(graph, id) {
293
+ return graph.nodes.find((n) => n.id === id);
476
294
  }
477
- async function laneInventory() {
478
- return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
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;
316
+ }
317
+ }
318
+ return edges;
479
319
  }
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
- ]);
498
- });
499
-
500
- // src/service.ts
501
- import { createHash as createHash3 } from "crypto";
502
- // package.json
503
- var package_default = {
504
- name: "@hasna/workflows",
505
- version: "0.2.0",
506
- description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
507
- type: "module",
508
- main: "./dist/index.js",
509
- types: "./types/index.d.ts",
510
- bin: {
511
- workflows: "dist/cli/index.js",
512
- "workflows-mcp": "dist/mcp/index.js",
513
- "workflows-serve": "dist/serve/index.js"
514
- },
515
- exports: {
516
- ".": {
517
- types: "./types/index.d.ts",
518
- import: "./dist/index.js"
519
- },
520
- "./sdk": {
521
- types: "./types/sdk.d.ts",
522
- import: "./dist/sdk.js"
523
- },
524
- "./cli": {
525
- types: "./types/cli/index.d.ts",
526
- import: "./dist/cli/index.js"
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))
329
+ continue;
330
+ seen.add(id);
331
+ const node = findNode(graph, id);
332
+ if (!node)
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);
527
347
  }
528
- },
529
- files: [
530
- "dist",
531
- "types",
532
- "README.md",
533
- "LICENSE",
534
- "hasna.contract.json"
535
- ],
536
- scripts: {
537
- build: "bun run check:sdk && bun run build:runtime && bun run build:types",
538
- "generate:sdk": "bun scripts/generate-sdk.ts --write",
539
- "check:sdk": "bun scripts/generate-sdk.ts",
540
- "test:artifacts": "bun scripts/artifact-acceptance.ts",
541
- "build:runtime": "bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/serve/index.ts src/serve/admin.ts --outdir dist/serve --target bun --packages external && bun build src/index.ts src/service.ts src/handlers.ts src/agent-tools.ts src/types.ts --root src --outdir dist --target bun --packages external && bun build src/sdk.ts --outdir dist --target node",
542
- "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir types",
543
- "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
544
- "contract:check": "bun scripts/contracts-cli.mjs repo-conformance .",
545
- typecheck: "tsc --noEmit",
546
- test: "bun test",
547
- "test:pg": 'WORKFLOWS_TEST_DATABASE_URL="${WORKFLOWS_TEST_DATABASE_URL:?point it at a fresh test Postgres database}" WORKFLOWS_REQUIRE_POSTGRES=1 bun test src/db/postgres-store.test.ts src/db/postgres-http.test.ts src/db/postgres-fencing.test.ts',
548
- prepack: "bun run build && bun run artifact-scan",
549
- prepublishOnly: "bun run test && bun run build"
550
- },
551
- keywords: [
552
- "workflows",
553
- "graph",
554
- "automation",
555
- "cli",
556
- "typescript",
557
- "bun",
558
- "hasna"
559
- ],
560
- publishConfig: {
561
- registry: "https://registry.npmjs.org",
562
- access: "public"
563
- },
564
- repository: {
565
- type: "git",
566
- url: "https://github.com/hasna/apps.git",
567
- directory: "apps/workflows"
568
- },
569
- homepage: "https://github.com/hasna/apps/tree/main/apps/workflows",
570
- bugs: {
571
- url: "https://github.com/hasna/apps/issues"
572
- },
573
- engines: {
574
- bun: ">=1.0.0"
575
- },
576
- author: "Hasna",
577
- license: "Apache-2.0",
578
- dependencies: {
579
- "@anthropic-ai/claude-agent-sdk": "0.3.234",
580
- "@cursor/sdk": "^1.0.28",
581
- "@openai/codex-sdk": "0.147.0",
582
- commander: "^13.1.0",
583
- "@hasna/contracts": "1.3.3",
584
- pg: "^8.16.3"
585
- },
586
- devDependencies: {
587
- "@types/bun": "1.3.14",
588
- "@types/pg": "^8.15.6",
589
- typescript: "^5.7.3"
590
348
  }
591
- };
592
-
593
- // src/version.ts
594
- function packageVersion() {
595
- return package_default.version;
349
+ return [...seen];
596
350
  }
597
-
598
- // src/daemon.ts
599
- import { randomUUID as randomUUID2 } from "crypto";
600
-
601
- // src/expr.ts
602
- class ExprSyntaxError extends Error {
603
- constructor(message) {
604
- super(message);
605
- this.name = "ExprSyntaxError";
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);
372
+ }
373
+ }
374
+ stack.pop();
375
+ color.set(id, "black");
606
376
  }
607
- }
608
-
609
- class ExprEvalError extends Error {
610
- constructor(message) {
611
- super(message);
612
- this.name = "ExprEvalError";
377
+ for (const node of graph.nodes) {
378
+ if (color.get(node.id) === undefined)
379
+ visit(node.id);
613
380
  }
381
+ return cycles;
614
382
  }
615
- var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
616
- function tokenize(input) {
617
- const tokens = [];
618
- let i = 0;
619
- while (i < input.length) {
620
- const ch = input[i];
621
- if (ch === " " || ch === "\t" || ch === `
622
- ` || ch === "\r") {
623
- i++;
624
- continue;
625
- }
626
- if (ch === "(") {
627
- tokens.push({ kind: "lparen", value: "(" });
628
- i++;
629
- continue;
630
- }
631
- if (ch === ")") {
632
- tokens.push({ kind: "rparen", value: ")" });
633
- i++;
634
- continue;
635
- }
636
- if (ch === '"' || ch === "'") {
637
- const quote = ch;
638
- let j = i + 1;
639
- let out = "";
640
- let closed = false;
641
- while (j < input.length) {
642
- if (input[j] === quote) {
643
- closed = true;
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" }] };
387
+ }
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");
644
401
  break;
645
402
  }
646
- out += input[j];
647
- j++;
648
403
  }
649
- if (!closed)
650
- throw new ExprSyntaxError(`unterminated string literal at offset ${i}`);
651
- tokens.push({ kind: "string", value: out });
652
- i = j + 1;
653
- continue;
654
404
  }
655
- const two = input.slice(i, i + 2);
656
- if (COMPARISON_OPS.has(two)) {
657
- tokens.push({ kind: "op", value: two });
658
- i += 2;
405
+ }
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 };
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");
659
414
  continue;
660
415
  }
661
- if (ch === "<" || ch === ">" || ch === "=" || ch === "!") {
662
- tokens.push({ kind: "op", value: ch });
663
- i++;
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 };
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)}`);
664
440
  continue;
665
441
  }
666
- if (/[0-9]/.test(ch)) {
667
- let j = i;
668
- while (j < input.length && /[0-9.]/.test(input[j]))
669
- j++;
670
- tokens.push({ kind: "number", value: input.slice(i, j) });
671
- i = j;
442
+ if (seenIds.has(node.id)) {
443
+ issue(issues, `nodes.${node.id}`, `duplicate node id ${node.id}`);
672
444
  continue;
673
445
  }
674
- if (/[A-Za-z_]/.test(ch)) {
675
- let j = i;
676
- while (j < input.length && /[A-Za-z0-9_.]/.test(input[j]))
677
- j++;
678
- tokens.push({ kind: "ident", value: input.slice(i, j) });
679
- i = j;
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)}`);
680
449
  continue;
681
450
  }
682
- throw new ExprSyntaxError(`unexpected character ${JSON.stringify(ch)} at offset ${i}`);
451
+ validateNode(graph, node, issues);
452
+ }
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
+ }
459
+ }
460
+ }
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
+ }
467
+ }
468
+ }
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)`);
472
+ }
473
+ return { ok: issues.length === 0, issues };
474
+ }
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 [];
491
+ }
492
+ }
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");
502
+ }
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(", ")})`);
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
+ }
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");
521
+ }
522
+ break;
523
+ }
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");
561
+ }
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
+ }
586
+ }
587
+ break;
588
+ }
589
+ case "end":
590
+ break;
683
591
  }
684
- return tokens;
685
592
  }
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
+ });
686
601
 
687
- class Parser {
688
- tokens;
689
- pos = 0;
690
- constructor(tokens) {
691
- this.tokens = tokens;
692
- }
693
- peek() {
694
- return this.tokens[this.pos];
695
- }
696
- next() {
697
- return this.tokens[this.pos++];
698
- }
699
- expectOp(value) {
700
- const t = this.next();
701
- if (!t || t.kind !== "op" || t.value !== value) {
702
- throw new ExprSyntaxError(`expected ${JSON.stringify(value)}`);
703
- }
704
- }
705
- parse() {
706
- if (this.tokens.length === 0)
707
- throw new ExprSyntaxError("empty expression");
708
- const node = this.parseOr();
709
- if (this.pos < this.tokens.length) {
710
- throw new ExprSyntaxError(`unexpected trailing token ${JSON.stringify(this.peek().value)}`);
711
- }
712
- return node;
713
- }
714
- parseOr() {
715
- let left = this.parseAnd();
716
- while (this.peek()?.kind === "ident" && this.peek().value === "or") {
717
- this.next();
718
- const right = this.parseAnd();
719
- left = { kind: "or", left, right };
720
- }
721
- return left;
602
+ // src/lanes/types.ts
603
+ import { statSync as statSync2 } 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 = statSync2(`${entry}/${binary}`);
609
+ if (stat.isFile())
610
+ return true;
611
+ } catch {}
722
612
  }
723
- parseAnd() {
724
- let left = this.parseNot();
725
- while (this.peek()?.kind === "ident" && this.peek().value === "and") {
726
- this.next();
727
- const right = this.parseNot();
728
- left = { kind: "and", left, right };
729
- }
730
- return left;
613
+ return false;
614
+ }
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;
731
622
  }
732
- parseNot() {
733
- if (this.peek()?.kind === "ident" && this.peek().value === "not") {
734
- this.next();
735
- return { kind: "not", operand: this.parseNot() };
736
- }
737
- return this.parseComparison();
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;
738
627
  }
739
- parseComparison() {
740
- const left = this.parsePrimary();
741
- const t = this.peek();
742
- if (t && t.kind === "op" && COMPARISON_OPS.has(t.value)) {
743
- this.next();
744
- const right = this.parsePrimary();
745
- return { kind: "comparison", op: t.value, left, right };
628
+ return env;
629
+ }
630
+ function subscriptionSdkEnvironment(overrides = {}) {
631
+ const safe = subscriptionEnvironment(overrides);
632
+ return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
633
+ }
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}`);
746
667
  }
747
- return left;
668
+ return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
748
669
  }
749
- parsePrimary() {
750
- const t = this.next();
751
- if (!t)
752
- throw new ExprSyntaxError("unexpected end of expression");
753
- if (t.kind === "lparen") {
754
- const inner = this.parseOr();
755
- this.expectCloseParen();
756
- return inner;
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";
757
692
  }
758
- if (t.kind === "number") {
759
- const value = Number(t.value);
760
- if (!Number.isFinite(value))
761
- throw new ExprSyntaxError(`invalid number ${JSON.stringify(t.value)}`);
762
- return { kind: "literal", value };
693
+ };
694
+ LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
695
+ constructor(message) {
696
+ super(message);
697
+ this.name = "LaneAdapterShapeError";
763
698
  }
764
- if (t.kind === "string")
765
- return { kind: "literal", value: t.value };
766
- if (t.kind === "ident") {
767
- if (t.value === "true")
768
- return { kind: "literal", value: true };
769
- if (t.value === "false")
770
- return { kind: "literal", value: false };
771
- return { kind: "path", segments: t.value.split(".") };
699
+ };
700
+ LaneTimeoutError = class LaneTimeoutError extends Error {
701
+ constructor(what, timeoutMs) {
702
+ super(`${what} exceeded the ${timeoutMs}ms budget`);
703
+ this.name = "LaneTimeoutError";
772
704
  }
773
- throw new ExprSyntaxError(`unexpected token ${JSON.stringify(t.value)}`);
774
- }
775
- expectCloseParen() {
776
- const t = this.next();
777
- if (!t || t.kind !== "rparen")
778
- throw new ExprSyntaxError("expected )");
779
- }
780
- }
781
- function parseExpr(input) {
782
- return new Parser(tokenize(input)).parse();
783
- }
784
- function resolvePath(segments, context) {
785
- let current = context;
786
- for (const segment of segments) {
787
- if (current === null || typeof current !== "object")
788
- return;
789
- const record = current;
790
- if (!(segment in record))
791
- return;
792
- current = record[segment];
793
- }
794
- if (typeof current === "number" || typeof current === "string" || typeof current === "boolean") {
795
- return current;
796
- }
797
- return;
798
- }
799
- function evalNode(node, context) {
800
- switch (node.kind) {
801
- case "literal":
802
- return node.value;
803
- case "path":
804
- return resolvePath(node.segments, context);
805
- case "comparison": {
806
- const left = evalNode(node.left, context);
807
- const right = evalNode(node.right, context);
808
- if (left === undefined || right === undefined) {
809
- if (node.op === "==")
810
- return left === undefined && right === undefined;
811
- if (node.op === "!=")
812
- return left !== right;
813
- throw new ExprEvalError("cannot order-compare an unknown path");
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
+ };
814
727
  }
815
- if (typeof left !== typeof right) {
816
- throw new ExprEvalError(`cannot compare ${JSON.stringify(left)} (${typeof left}) with ${JSON.stringify(right)} (${typeof right})`);
728
+ },
729
+ async run(job) {
730
+ if (!job.prompt) {
731
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
817
732
  }
818
- switch (node.op) {
819
- case "==":
820
- return left === right;
821
- case "!=":
822
- return left !== right;
823
- case "<":
824
- return left < right;
825
- case "<=":
826
- return left <= right;
827
- case ">":
828
- return left > right;
829
- case ">=":
830
- return left >= right;
831
- default:
832
- throw new ExprEvalError(`unknown operator ${node.op}`);
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);
833
741
  }
834
- }
835
- case "and": {
836
- const left = evalNode(node.left, context);
837
- const right = evalNode(node.right, context);
838
- if (typeof left !== "boolean" || typeof right !== "boolean") {
839
- throw new ExprEvalError("and requires boolean operands");
742
+ const query = sdk.query;
743
+ if (typeof query !== "function") {
744
+ throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
840
745
  }
841
- return left && right;
842
- }
843
- case "or": {
844
- const left = evalNode(node.left, context);
845
- const right = evalNode(node.right, context);
846
- if (typeof left !== "boolean" || typeof right !== "boolean") {
847
- throw new ExprEvalError("or requires boolean operands");
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) };
848
777
  }
849
- return left || right;
850
- }
851
- case "not": {
852
- const operand = evalNode(node.operand, context);
853
- if (typeof operand !== "boolean")
854
- throw new ExprEvalError("not requires a boolean operand");
855
- return !operand;
856
- }
857
- }
858
- }
859
- function evaluateExpr(input, context) {
860
- const ast = parseExpr(input);
861
- const value = evalNode(ast, context);
862
- if (typeof value !== "boolean") {
863
- throw new ExprEvalError(`condition must evaluate to a boolean, got ${typeof value}`);
864
- }
865
- return value;
866
- }
867
-
868
- // src/graph.ts
869
- var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
870
- var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
871
- var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
872
- function issue(issues, path, message) {
873
- issues.push({ path, message });
874
- }
875
- function findNode(graph, id) {
876
- return graph.nodes.find((n) => n.id === id);
877
- }
878
- function collectEdges(graph) {
879
- const edges = [];
880
- for (const node of graph.nodes) {
881
- switch (node.type) {
882
- case "start":
883
- case "step":
884
- if (node.next)
885
- edges.push({ from: node.id, to: node.next });
886
- break;
887
- case "decision":
888
- if (node.then)
889
- edges.push({ from: node.id, to: node.then });
890
- if (node.else)
891
- edges.push({ from: node.id, to: node.else });
892
- break;
893
- case "while":
894
- if (node.next)
895
- edges.push({ from: node.id, to: node.next });
896
- break;
897
- case "end":
898
- break;
899
778
  }
900
- }
901
- return edges;
779
+ };
902
780
  }
903
- function reachableNodes(graph) {
904
- const start = graph.nodes.find((n) => n.type === "start");
905
- if (!start)
906
- return [];
907
- const seen = new Set;
908
- const stack = [start.id];
909
- while (stack.length > 0) {
910
- const id = stack.pop();
911
- if (seen.has(id))
912
- continue;
913
- seen.add(id);
914
- const node = findNode(graph, id);
915
- if (!node)
916
- continue;
917
- if (node.type === "start" || node.type === "step") {
918
- if (node.next)
919
- stack.push(node.next);
920
- } else if (node.type === "decision") {
921
- if (node.then)
922
- stack.push(node.then);
923
- if (node.else)
924
- stack.push(node.else);
925
- } else if (node.type === "while") {
926
- for (const bodyId of node.body)
927
- stack.push(bodyId);
928
- if (node.next)
929
- stack.push(node.next);
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 };
930
794
  }
795
+ throw err;
931
796
  }
932
- return [...seen];
933
797
  }
934
- function findCycles(graph) {
935
- const edges = collectEdges(graph);
936
- const adj = new Map;
937
- for (const e of edges) {
938
- const list = adj.get(e.from) ?? [];
939
- list.push(e.to);
940
- adj.set(e.from, list);
941
- }
942
- const color = new Map;
943
- const stack = [];
944
- const cycles = [];
945
- function visit(id) {
946
- color.set(id, "grey");
947
- stack.push(id);
948
- for (const to of adj.get(id) ?? []) {
949
- const c = color.get(to);
950
- if (c === "grey") {
951
- const idx = stack.indexOf(to);
952
- cycles.push([...stack.slice(idx), to]);
953
- } else if (c === undefined) {
954
- visit(to);
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" };
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) };
955
867
  }
956
868
  }
957
- stack.pop();
958
- color.set(id, "black");
959
- }
960
- for (const node of graph.nodes) {
961
- if (color.get(node.id) === undefined)
962
- visit(node.id);
963
- }
964
- return cycles;
869
+ };
965
870
  }
966
- function validateGraph(graph) {
967
- const issues = [];
968
- if (graph === null || typeof graph !== "object" || Array.isArray(graph)) {
969
- return { ok: false, issues: [{ path: "graph", message: "graph must be an object" }] };
970
- }
971
- if (typeof graph.name !== "string" || graph.name.trim() === "") {
972
- issue(issues, "name", "graph requires a non-empty name");
973
- }
974
- if (typeof graph.version !== "string" || graph.version.trim() === "") {
975
- issue(issues, "version", "graph requires a version string");
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;
976
889
  }
977
- if (graph.memoWatch !== undefined) {
978
- if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
979
- issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
980
- } else {
981
- for (const entry of graph.memoWatch) {
982
- if (typeof entry !== "string" || entry.trim() === "") {
983
- issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
984
- break;
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" };
985
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 run2 = await withTimeout(runLocalAgent(sdk, job, (callback) => {
941
+ cancel = callback;
942
+ }), timeoutMs, "cursor lane", () => cancel?.());
943
+ const durationMs = Date.now() - started;
944
+ const status = run2.status;
945
+ const ok = status === "finished";
946
+ return { ok, exitCode: ok ? 0 : 1, output: run2.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) };
986
952
  }
987
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");
988
961
  }
989
- if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
990
- issue(issues, "nodes", "graph requires at least one node");
991
- return { ok: false, issues };
992
- }
993
- for (let index = 0;index < graph.nodes.length; index++) {
994
- const node = graph.nodes[index];
995
- if (node === null || typeof node !== "object" || Array.isArray(node)) {
996
- issue(issues, `nodes.${index}`, "node must be an object");
997
- continue;
998
- }
999
- if (!GRAPH_NODE_TYPES.includes(node.type))
1000
- issue(issues, `nodes.${index}.type`, "type must be start, step, decision, while, or end");
1001
- for (const key of ["next", "then", "else", "prompt", "command"]) {
1002
- const value = node[key];
1003
- if (value !== undefined && (typeof value !== "string" || value.trim() === ""))
1004
- issue(issues, `nodes.${index}.${key}`, `${key} must be a non-empty string`);
1005
- }
1006
- if (node.type === "while" && (!Array.isArray(node.body) || node.body.some((id) => typeof id !== "string")))
1007
- issue(issues, `nodes.${index}.body`, "body must be an array of node ids");
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()");
1008
966
  }
1009
- if (issues.length)
1010
- return { ok: false, issues };
1011
- const starts = graph.nodes.filter((n) => n.type === "start");
1012
- if (starts.length === 0)
1013
- issue(issues, "nodes", "graph requires exactly one start node (found none)");
1014
- if (starts.length > 1)
1015
- issue(issues, "nodes", "graph requires exactly one start node (found multiple)");
1016
- const ends = graph.nodes.filter((n) => n.type === "end");
1017
- if (ends.length === 0)
1018
- issue(issues, "nodes", "graph requires at least one end node");
1019
- const seenIds = new Map;
1020
- for (const node of graph.nodes) {
1021
- if (typeof node.id !== "string" || !NODE_ID_RE.test(node.id)) {
1022
- issue(issues, "nodes", `node id ${JSON.stringify(node.id)} must match ${String(NODE_ID_RE)}`);
1023
- continue;
1024
- }
1025
- if (seenIds.has(node.id)) {
1026
- issue(issues, `nodes.${node.id}`, `duplicate node id ${node.id}`);
1027
- continue;
1028
- }
1029
- seenIds.set(node.id, node);
1030
- if (!GRAPH_NODE_TYPES.includes(node.type)) {
1031
- issue(issues, `nodes.${node.id}`, `unknown node type ${JSON.stringify(node.type)}`);
1032
- continue;
1033
- }
1034
- validateNode(graph, node, issues);
967
+ let run2 = await agent.send(job.prompt, { model: job.model });
968
+ if (run2 && typeof run2 === "object") {
969
+ const handle = run2;
970
+ if (typeof handle.cancel === "function")
971
+ onCancel(() => {
972
+ handle.cancel().catch(() => {});
973
+ });
974
+ if (typeof handle.wait === "function")
975
+ run2 = await handle.wait();
1035
976
  }
1036
- for (const node of graph.nodes) {
1037
- const edges = nodeEdges(node);
1038
- for (const { kind, target } of edges) {
1039
- if (!seenIds.has(target)) {
1040
- issue(issues, `nodes.${node.id}`, `${kind} edge targets nonexistent node ${target}`);
1041
- }
1042
- }
977
+ if (!run2 || typeof run2 !== "object") {
978
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
1043
979
  }
1044
- if (starts.length === 1) {
1045
- const reachable = new Set(reachableNodes(graph));
1046
- for (const node of graph.nodes) {
1047
- if (!reachable.has(node.id)) {
1048
- issue(issues, `nodes.${node.id}`, `node ${node.id} is unreachable from start`);
1049
- }
1050
- }
980
+ const candidate = run2;
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)`);
1051
983
  }
1052
- const cycles = findCycles(graph);
1053
- for (const cycle of cycles) {
1054
- issue(issues, "nodes", `explicit cycle detected: ${cycle.join(" -> ")} (loop only via a while node)`);
984
+ if (typeof candidate.result !== "string") {
985
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
1055
986
  }
1056
- return { ok: issues.length === 0, issues };
987
+ return { status: candidate.status, result: candidate.result };
1057
988
  }
1058
- function nodeEdges(node) {
1059
- switch (node.type) {
1060
- case "start":
1061
- case "step":
1062
- return node.next ? [{ kind: "next", target: node.next }] : [];
1063
- case "decision":
1064
- return [
1065
- ...node.then ? [{ kind: "then", target: node.then }] : [],
1066
- ...node.else ? [{ kind: "else", target: node.else }] : []
1067
- ];
1068
- case "while":
1069
- return node.next ? [{ kind: "next", target: node.next }] : [];
1070
- case "end":
1071
- return [];
1072
- default:
1073
- return [];
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 };
1002
+ }
1003
+ throw err;
1074
1004
  }
1075
1005
  }
1076
- function validateNode(graph, node, issues) {
1077
- switch (node.type) {
1078
- case "start":
1079
- if (!node.next)
1080
- issue(issues, `nodes.${node.id}`, "start node requires a next edge");
1081
- break;
1082
- case "step": {
1083
- if (!node.prompt && !node.command) {
1084
- issue(issues, `nodes.${node.id}`, "step node requires prompt or command");
1085
- }
1086
- if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
1087
- issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
1088
- }
1089
- if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
1090
- issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
1091
- }
1092
- if (node.timeoutMs !== undefined && (!Number.isInteger(node.timeoutMs) || node.timeoutMs < 1)) {
1093
- issue(issues, `nodes.${node.id}`, "timeoutMs must be a positive integer");
1094
- }
1095
- break;
1096
- }
1097
- case "decision": {
1098
- if (typeof node.condition !== "string" || node.condition.trim() === "") {
1099
- issue(issues, `nodes.${node.id}`, "decision node requires a condition expression");
1100
- } else {
1101
- try {
1102
- parseExpr(node.condition);
1103
- } catch (err) {
1104
- if (err instanceof ExprSyntaxError) {
1105
- issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
1106
- } else {
1107
- throw err;
1108
- }
1109
- }
1110
- }
1111
- if (!node.then && !node.else) {
1112
- issue(issues, `nodes.${node.id}`, "decision node requires then and/or else edge");
1113
- }
1114
- break;
1115
- }
1116
- case "while": {
1117
- if (typeof node.condition !== "string" || node.condition.trim() === "") {
1118
- issue(issues, `nodes.${node.id}`, "while node requires a condition expression");
1119
- } else {
1120
- try {
1121
- parseExpr(node.condition);
1122
- } catch (err) {
1123
- if (err instanceof ExprSyntaxError) {
1124
- issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
1125
- } else {
1126
- throw err;
1127
- }
1128
- }
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" };
1129
1018
  }
1130
- if (node.maxIterations === undefined) {
1131
- issue(issues, `nodes.${node.id}`, "while node requires maxIterations (finite loop bound)");
1132
- } else if (!Number.isInteger(node.maxIterations) || node.maxIterations < 1) {
1133
- 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" };
1134
1029
  }
1135
- if (!Array.isArray(node.body) || node.body.length === 0) {
1136
- issue(issues, `nodes.${node.id}`, "while node requires a non-empty body");
1137
- } else {
1138
- for (const bodyId of node.body) {
1139
- const bodyNode = findNode(graph, bodyId);
1140
- if (!bodyNode) {
1141
- issue(issues, `nodes.${node.id}`, `while body references nonexistent node ${bodyId}`);
1142
- continue;
1143
- }
1144
- if (bodyNode.type === "start" || bodyNode.type === "end" || bodyNode.type === "while") {
1145
- issue(issues, `nodes.${node.id}`, `while body may only contain step/decision nodes in v1 (found ${bodyNode.type})`);
1146
- }
1147
- }
1148
- const bodySet = new Set(node.body);
1149
- for (const bodyId of node.body) {
1150
- const bodyNode = findNode(graph, bodyId);
1151
- if (!bodyNode)
1152
- continue;
1153
- for (const { kind, target } of nodeEdges(bodyNode)) {
1154
- if (!bodySet.has(target)) {
1155
- issue(issues, `nodes.${node.id}`, `while body node ${bodyId} ${kind}-edge leaves the body (target ${target})`);
1156
- }
1157
- }
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 };
1158
1045
  }
1046
+ throw err;
1159
1047
  }
1160
- break;
1161
1048
  }
1162
- case "end":
1163
- break;
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)));
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
+ });
1105
+
1106
+ // src/service.ts
1107
+ import { createHash as createHash3 } from "crypto";
1108
+ // package.json
1109
+ var package_default = {
1110
+ name: "@hasna/workflows",
1111
+ version: "0.2.2",
1112
+ description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
1113
+ type: "module",
1114
+ main: "./dist/index.js",
1115
+ types: "./types/index.d.ts",
1116
+ bin: {
1117
+ workflows: "dist/cli/index.js",
1118
+ "workflows-mcp": "dist/mcp/index.js",
1119
+ "workflows-serve": "dist/serve/index.js"
1120
+ },
1121
+ exports: {
1122
+ ".": {
1123
+ types: "./types/index.d.ts",
1124
+ import: "./dist/index.js"
1125
+ },
1126
+ "./sdk": {
1127
+ types: "./types/sdk.d.ts",
1128
+ import: "./dist/sdk.js"
1129
+ },
1130
+ "./cli": {
1131
+ types: "./types/cli/index.d.ts",
1132
+ import: "./dist/cli/index.js"
1133
+ }
1134
+ },
1135
+ files: [
1136
+ "dist",
1137
+ "types",
1138
+ "README.md",
1139
+ "LICENSE",
1140
+ "hasna.contract.json"
1141
+ ],
1142
+ scripts: {
1143
+ build: "bun run check:sdk && bun run build:runtime && bun run build:types",
1144
+ "generate:sdk": "bun scripts/generate-sdk.ts --write",
1145
+ "check:sdk": "bun scripts/generate-sdk.ts",
1146
+ "test:artifacts": "bun scripts/artifact-acceptance.ts",
1147
+ "build:runtime": "bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/serve/index.ts src/serve/admin.ts --outdir dist/serve --target bun --packages external && bun build src/index.ts src/service.ts src/handlers.ts src/agent-tools.ts src/types.ts --root src --outdir dist --target bun --packages external && bun build src/sdk.ts --outdir dist --target node",
1148
+ "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir types",
1149
+ "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
1150
+ "contract:check": "bun scripts/contracts-cli.mjs repo-conformance .",
1151
+ typecheck: "tsc --noEmit",
1152
+ test: "bun test",
1153
+ "test:pg": 'WORKFLOWS_TEST_DATABASE_URL="${WORKFLOWS_TEST_DATABASE_URL:?point it at a fresh test Postgres database}" WORKFLOWS_REQUIRE_POSTGRES=1 bun test src/db/postgres-store.test.ts src/db/postgres-http.test.ts src/db/postgres-fencing.test.ts',
1154
+ prepack: "bun run build && bun run artifact-scan",
1155
+ prepublishOnly: "bun run test && bun run build"
1156
+ },
1157
+ keywords: [
1158
+ "workflows",
1159
+ "graph",
1160
+ "automation",
1161
+ "cli",
1162
+ "typescript",
1163
+ "bun",
1164
+ "hasna"
1165
+ ],
1166
+ publishConfig: {
1167
+ registry: "https://registry.npmjs.org",
1168
+ access: "public"
1169
+ },
1170
+ repository: {
1171
+ type: "git",
1172
+ url: "https://github.com/hasna/apps.git",
1173
+ directory: "apps/workflows"
1174
+ },
1175
+ homepage: "https://github.com/hasna/apps/tree/main/apps/workflows",
1176
+ bugs: {
1177
+ url: "https://github.com/hasna/apps/issues"
1178
+ },
1179
+ engines: {
1180
+ bun: ">=1.0.0"
1181
+ },
1182
+ author: "Hasna",
1183
+ license: "Apache-2.0",
1184
+ dependencies: {
1185
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
1186
+ "@cursor/sdk": "^1.0.28",
1187
+ "@hasna/contracts": "1.3.5",
1188
+ "@openai/codex-sdk": "0.160.0",
1189
+ commander: "^13.1.0",
1190
+ pg: "^8.16.3"
1191
+ },
1192
+ devDependencies: {
1193
+ "@types/bun": "1.3.14",
1194
+ "@types/pg": "^8.15.6",
1195
+ typescript: "^5.7.3"
1164
1196
  }
1197
+ };
1198
+
1199
+ // src/version.ts
1200
+ function packageVersion() {
1201
+ return package_default.version;
1165
1202
  }
1166
1203
 
1204
+ // src/daemon.ts
1205
+ init_expr();
1206
+ init_graph();
1207
+ import { randomUUID as randomUUID2 } from "crypto";
1208
+
1167
1209
  // src/secrets.ts
1168
1210
  class SecretsGateError extends Error {
1169
1211
  constructor(message) {
@@ -2034,7 +2076,14 @@ class WorkflowsDaemon {
2034
2076
  const isLoopBody = graph.nodes.some((candidate) => candidate.type === "while" && candidate.body.includes(nodeId));
2035
2077
  const nodeRow = latest === undefined || isLoopBody && latest.status === "completed" ? await this.store.createRunNode({ runId: run2.id, nodeId, lane: node2.lane ?? "claude" }) : latest;
2036
2078
  const memoWatch = node2.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
2037
- const memoSource = { command: node2.command, prompt: node2.prompt, context: stripRunState(context) };
2079
+ const memoSource = {
2080
+ command: node2.command,
2081
+ prompt: node2.prompt,
2082
+ context: stripRunState(context),
2083
+ lane: node2.lane ?? "claude",
2084
+ cwd: node2.cwd,
2085
+ codexMode: node2.lane === "codex" ? node2.codexMode ?? "repository" : undefined
2086
+ };
2038
2087
  const memoInput = node2.memo ? memoWatch !== undefined ? { ...memoSource, memoWatch } : memoSource : undefined;
2039
2088
  if (memoInput !== undefined) {
2040
2089
  const hit = await tryMemoHit(this.store, run2.graphName, nodeId, memoInput);
@@ -2059,6 +2108,8 @@ class WorkflowsDaemon {
2059
2108
  lane: node2.lane ?? "claude",
2060
2109
  prompt: node2.prompt,
2061
2110
  command: node2.command,
2111
+ cwd: node2.cwd,
2112
+ codexMode: node2.codexMode,
2062
2113
  timeoutMs: node2.timeoutMs,
2063
2114
  env: this.env
2064
2115
  };
@@ -2644,7 +2695,7 @@ function openStore(dataDir2) {
2644
2695
  // src/wal.ts
2645
2696
  import { Database as Database2 } from "bun:sqlite";
2646
2697
  import { createHash as createHash2 } from "crypto";
2647
- import { appendFileSync, copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, truncateSync, writeFileSync, constants } from "fs";
2698
+ import { closeSync, copyFileSync, existsSync as existsSync2, fsyncSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, renameSync, truncateSync, writeFileSync, constants } from "fs";
2648
2699
  import { basename, dirname, join as join4 } from "path";
2649
2700
  var WAL_FILE_NAME = "session.wal";
2650
2701
  var SESSIONS_DIR_NAME = "sessions";
@@ -2714,8 +2765,20 @@ class SessionWAL {
2714
2765
  const full = { ...op, at };
2715
2766
  const nextSeq = this.nextSeq();
2716
2767
  const entry = { seq: nextSeq, at, checksum: checksumOf(full), op: full };
2717
- appendFileSync(this.filePath, JSON.stringify(entry) + `
2718
- `, { encoding: "utf8", mode: 384 });
2768
+ const fd = openSync(this.filePath, "a", 384);
2769
+ try {
2770
+ writeFileSync(fd, JSON.stringify(entry) + `
2771
+ `, "utf8");
2772
+ fsyncSync(fd);
2773
+ } finally {
2774
+ closeSync(fd);
2775
+ }
2776
+ const parent = openSync(dirname(this.filePath), "r");
2777
+ try {
2778
+ fsyncSync(parent);
2779
+ } finally {
2780
+ closeSync(parent);
2781
+ }
2719
2782
  return entry;
2720
2783
  }
2721
2784
  nextSeq() {
@@ -2732,7 +2795,9 @@ class SessionWAL {
2732
2795
  let content;
2733
2796
  try {
2734
2797
  content = readFileSync2(this.filePath, "utf8");
2735
- } catch {
2798
+ } catch (error) {
2799
+ if (error.code !== "ENOENT")
2800
+ throw error;
2736
2801
  return { entries, torn, repaired, liveClaims: () => new Map };
2737
2802
  }
2738
2803
  if (content.length > MAX_WAL_BYTES) {
@@ -2822,7 +2887,42 @@ function preserveOriginal(filePath) {
2822
2887
  return backup;
2823
2888
  }
2824
2889
 
2890
+ // src/journal-directory.ts
2891
+ import { accessSync, closeSync as closeSync2, constants as constants2, fstatSync, lstatSync, openSync as openSync2, readFileSync as readFileSync3, readSync } from "fs";
2892
+ import { join as join5 } from "path";
2893
+ function assertJournalDirectory(dataDir2, identity) {
2894
+ try {
2895
+ if (!lstatSync(dataDir2).isDirectory() || !lstatSync(join5(dataDir2, "sessions")).isDirectory())
2896
+ throw new Error;
2897
+ accessSync(dataDir2, constants2.R_OK | constants2.W_OK | constants2.X_OK);
2898
+ accessSync(join5(dataDir2, "sessions"), constants2.R_OK | constants2.W_OK | constants2.X_OK);
2899
+ const marker = openSync2(join5(dataDir2, ".workflows-data-identity"), constants2.O_RDONLY | constants2.O_NOFOLLOW);
2900
+ try {
2901
+ const stat = fstatSync(marker);
2902
+ if (!stat.isFile() || stat.size > 256 || readFileSync3(marker, "utf8") !== identity + `
2903
+ `)
2904
+ throw new Error;
2905
+ } finally {
2906
+ closeSync2(marker);
2907
+ }
2908
+ for (const file of ["workflows.db", "sessions/session.wal"]) {
2909
+ const fd = openSync2(join5(dataDir2, file), constants2.O_RDWR | constants2.O_NOFOLLOW);
2910
+ try {
2911
+ if (!fstatSync(fd).isFile())
2912
+ throw new Error;
2913
+ readSync(fd, Buffer.alloc(1), 0, 1, 0);
2914
+ } finally {
2915
+ closeSync2(fd);
2916
+ }
2917
+ }
2918
+ } catch {
2919
+ throw new Error("configured execution journal identity or accessibility check failed");
2920
+ }
2921
+ }
2922
+
2825
2923
  // src/service.ts
2924
+ init_graph();
2925
+ init_lanes();
2826
2926
  import { hostname } from "os";
2827
2927
  import { resolveDatabaseUrl } from "@hasna/contracts";
2828
2928
  var DEFAULT_PORT = 8790;
@@ -2843,6 +2943,14 @@ function envInt(names, fallback) {
2843
2943
  return Number.isFinite(parsed) ? parsed : fallback;
2844
2944
  }
2845
2945
  function resolveWorkflowsConfig(overrides = {}) {
2946
+ const rawLanes = process.env.HASNA_WORKFLOWS_ENABLED_LANES;
2947
+ const enabledLanes = overrides.enabledLanes ?? (rawLanes === undefined ? LANE_KINDS : rawLanes.trim() === "" ? [] : rawLanes.split(",").map((value) => value.trim()));
2948
+ if (!Array.isArray(enabledLanes) || enabledLanes.some((lane) => !LANE_KINDS.includes(lane)) || new Set(enabledLanes).size !== enabledLanes.length) {
2949
+ throw new Error("HASNA_WORKFLOWS_ENABLED_LANES must be a unique comma-separated subset of claude,codex,cursor,grok; an empty value disables model lanes");
2950
+ }
2951
+ const dataIdentity = overrides.dataIdentity ?? process.env.HASNA_WORKFLOWS_DATA_IDENTITY;
2952
+ if (dataIdentity !== undefined && !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(dataIdentity))
2953
+ throw new Error("HASNA_WORKFLOWS_DATA_IDENTITY must be a nonempty data identity of at most 128 characters");
2846
2954
  const originAuth = overrides.originAuth ?? process.env.HASNA_WORKFLOWS_ORIGIN_AUTH;
2847
2955
  if (originAuth !== undefined && !/^[A-Za-z0-9_-]{32,256}$/.test(originAuth))
2848
2956
  throw new Error("HASNA_WORKFLOWS_ORIGIN_AUTH must be a valid gateway origin identity");
@@ -2853,9 +2961,11 @@ function resolveWorkflowsConfig(overrides = {}) {
2853
2961
  apiUrl: overrides.apiUrl ?? envString(["HASNA_WORKFLOWS_API_URL", "WORKFLOWS_API_URL"]),
2854
2962
  apiKey: overrides.apiKey ?? envString(["HASNA_WORKFLOWS_API_KEY", "WORKFLOWS_API_KEY"]),
2855
2963
  originAuth,
2964
+ dataIdentity,
2856
2965
  storageBackend: overrides.storageBackend ?? "postgresql",
2857
2966
  databaseUrl: overrides.databaseUrl ?? envString(["HASNA_WORKFLOWS_DATABASE_URL", "WORKFLOWS_DATABASE_URL"]),
2858
- databaseSchema: overrides.databaseSchema ?? envString(["HASNA_WORKFLOWS_DATABASE_SCHEMA", "WORKFLOWS_DATABASE_SCHEMA"])
2967
+ databaseSchema: overrides.databaseSchema ?? envString(["HASNA_WORKFLOWS_DATABASE_SCHEMA", "WORKFLOWS_DATABASE_SCHEMA"]),
2968
+ enabledLanes: Object.freeze([...enabledLanes])
2859
2969
  };
2860
2970
  }
2861
2971
 
@@ -2888,16 +2998,52 @@ class WorkflowsService {
2888
2998
  }
2889
2999
  async ready() {
2890
3000
  const checks = { version: "ok", auth: this.config.apiKey ? "ok" : "missing", storage: "not-checked" };
2891
- if (this.config.apiKey) {
3001
+ if (this.config.dataIdentity !== undefined) {
3002
+ try {
3003
+ this.verifyJournalDirectory();
3004
+ checks.journal = "ok";
3005
+ } catch {
3006
+ checks.journal = "unavailable";
3007
+ }
3008
+ }
3009
+ if (this.config.apiKey && checks.journal !== "unavailable") {
2892
3010
  try {
2893
3011
  checks.storage = await this.withStore(async (store) => (store instanceof PostgresWorkflowsStore ? await store.checkSchema() : (await store.listTables()).sort().join(",") === "memos,run_nodes,runs") ? "ok" : "invalid-schema");
2894
3012
  } catch (error) {
2895
3013
  checks.storage = error instanceof WorkflowRequestError && error.code === "invalid_schema" ? "invalid-schema" : "unavailable";
2896
3014
  }
2897
3015
  }
2898
- return { ok: checks.auth === "ok" && checks.storage === "ok", service: this.name, version: this.version, checks };
3016
+ return { ok: checks.auth === "ok" && checks.storage === "ok" && checks.journal !== "unavailable", service: this.name, version: this.version, checks };
3017
+ }
3018
+ verifyJournalDirectory() {
3019
+ if (this.config.dataIdentity !== undefined) {
3020
+ try {
3021
+ assertJournalDirectory(this.config.dataDir, this.config.dataIdentity);
3022
+ } catch {
3023
+ throw new WorkflowRequestError("journal_unavailable", "configured execution journal identity or accessibility check failed", 503);
3024
+ }
3025
+ }
3026
+ }
3027
+ assertEnabledGraphLanes(graph) {
3028
+ if (!Array.isArray(graph?.nodes))
3029
+ return;
3030
+ for (const node2 of graph.nodes) {
3031
+ if (node2.type === "step" && !node2.command && !this.config.enabledLanes.includes(node2.lane ?? "claude")) {
3032
+ throw new WorkflowRequestError("lane_disabled", "the workflow contains a model lane disabled by server configuration", 403);
3033
+ }
3034
+ }
3035
+ }
3036
+ async probeLane(kind) {
3037
+ if (this.config.enabledLanes.includes(kind))
3038
+ return probeLane(kind);
3039
+ const sdk = { claude: "@anthropic-ai/claude-agent-sdk", codex: "@openai/codex-sdk", cursor: "@cursor/sdk", grok: "no npm SDK (measured 2026-08-25)" };
3040
+ return { kind, sdk: sdk[kind], wired: false, reason: "disabled by server configuration" };
3041
+ }
3042
+ laneInventory() {
3043
+ return Promise.all(LANE_KINDS.map((kind) => this.probeLane(kind)));
2899
3044
  }
2900
3045
  async openEngineStore(verifySchema = true) {
3046
+ this.verifyJournalDirectory();
2901
3047
  if (this.config.storageBackend === "sqlite")
2902
3048
  return openStore(this.config.dataDir);
2903
3049
  if (this.config.storageBackend !== "postgresql")
@@ -2951,6 +3097,7 @@ class WorkflowsService {
2951
3097
  return this.withStore((store) => store.listRunNodes(id));
2952
3098
  }
2953
3099
  events(id) {
3100
+ this.verifyJournalDirectory();
2954
3101
  return SessionWAL.open(this.config.dataDir).replay({ repair: false }).entries.filter((entry) => ("runId" in entry.op) && entry.op.runId === id);
2955
3102
  }
2956
3103
  cancelRun(id) {
@@ -2967,7 +3114,6 @@ class WorkflowsService {
2967
3114
  }
2968
3115
  async resumeRun(id) {
2969
3116
  const store = await this.openEngineStore();
2970
- const wal = SessionWAL.open(this.config.dataDir);
2971
3117
  try {
2972
3118
  const run2 = await store.getRun(id);
2973
3119
  if (!run2)
@@ -2975,13 +3121,15 @@ class WorkflowsService {
2975
3121
  if (!["failed", "cancelled", "interrupted"].includes(run2.status))
2976
3122
  throw new WorkflowRequestError("invalid_run_status", "resume requires a failed, cancelled, or interrupted run", 409);
2977
3123
  const context = JSON.parse(run2.contextJson);
3124
+ const graph = context.__wf?.graph;
3125
+ if (!graph)
3126
+ throw new WorkflowRequestError("graph_unavailable", "legacy run needs its original graph registered by the server operator", 409);
3127
+ this.assertEnabledGraphLanes(graph);
3128
+ const wal = SessionWAL.open(this.config.dataDir);
2978
3129
  if (context.__wf?.resumeBlocked || context.__wf?.executionMayBeActive || wal.replay({ repair: false }).liveClaims().has(id))
2979
3130
  throw new WorkflowRequestError("execution_active", "previous execution must finish before resume", 409);
2980
3131
  if (store instanceof PostgresWorkflowsStore && !await store.acquireExecution(id))
2981
3132
  throw new WorkflowRequestError("execution_active", "another executor still owns this run", 409);
2982
- const graph = context.__wf?.graph;
2983
- if (!graph)
2984
- throw new WorkflowRequestError("graph_unavailable", "legacy run needs its original graph registered by the server operator", 409);
2985
3133
  if (!await store.compareRunStatus(id, ["failed", "cancelled", "interrupted"], "pending", { error: null }))
2986
3134
  throw new WorkflowRequestError("run_status_conflict", "run changed before resume", 409);
2987
3135
  for (const node2 of await store.listRunNodes(id)) {
@@ -2995,6 +3143,7 @@ class WorkflowsService {
2995
3143
  }
2996
3144
  }
2997
3145
  sessions(action) {
3146
+ this.verifyJournalDirectory();
2998
3147
  const replay = SessionWAL.open(this.config.dataDir).replay({ repair: false });
2999
3148
  if (action === "list")
3000
3149
  return replay.entries.slice(-100).map(({ seq, at, op }) => ({ seq, at, operation: op.op }));
@@ -3012,6 +3161,7 @@ class WorkflowsService {
3012
3161
  return { ...local, health: this.health(), sessions: this.sessions("pull") };
3013
3162
  }
3014
3163
  daemonStatus() {
3164
+ this.verifyJournalDirectory();
3015
3165
  const replay = SessionWAL.open(this.config.dataDir).replay({ repair: false });
3016
3166
  return { activeClaims: replay.liveClaims().size, mode: "bounded-trigger", continuousDaemon: false };
3017
3167
  }
@@ -3019,6 +3169,7 @@ class WorkflowsService {
3019
3169
  return this.withStore(async (store) => (await store.memoList()).slice(0, 100));
3020
3170
  }
3021
3171
  async triggerRun(graph, context = {}, opts = {}) {
3172
+ this.assertEnabledGraphLanes(graph);
3022
3173
  const store = await this.openEngineStore();
3023
3174
  const wal = SessionWAL.open(this.config.dataDir);
3024
3175
  try {
@@ -3150,7 +3301,7 @@ function buildOpenApi(version) {
3150
3301
  ReadinessReport: object({ ok: { type: "boolean" }, service: string, version: string, checks: { type: "object", additionalProperties: string } }),
3151
3302
  GraphValidation: object({ ok: { type: "boolean" }, issues: { type: "array", items: object({ path: string, message: string }) } }),
3152
3303
  StartNode: object({ id: string, type: { type: "string", enum: ["start"] }, next: string }, ["id", "type"]),
3153
- StepNode: object({ id: string, type: { type: "string", enum: ["step"] }, next: string, lane: { type: "string", enum: ["claude", "codex", "cursor", "grok"] }, prompt: string, command: string, maxRetries: { type: "integer", minimum: 0 }, memo: { type: "boolean" }, timeoutMs: { type: "integer", minimum: 1 } }, ["id", "type"]),
3304
+ StepNode: object({ id: string, type: { type: "string", enum: ["step"] }, next: string, lane: { type: "string", enum: ["claude", "codex", "cursor", "grok"] }, prompt: string, command: string, cwd: { type: "string", minLength: 1 }, codexMode: { type: "string", enum: ["repository", "read-only"], default: "repository", description: "Codex prompt steps only. Read-only permits nonrepository execution with a read-only sandbox and no approvals. Repository retains native trust, sandbox and approval defaults." }, maxRetries: { type: "integer", minimum: 0 }, memo: { type: "boolean" }, timeoutMs: { type: "integer", minimum: 1 } }, ["id", "type"]),
3154
3305
  DecisionNode: object({ id: string, type: { type: "string", enum: ["decision"] }, condition: string, then: string, else: string }, ["id", "type", "condition"]),
3155
3306
  WhileNode: object({ id: string, type: { type: "string", enum: ["while"] }, condition: string, body: { type: "array", items: string }, maxIterations: { type: "integer", minimum: 1 }, next: string }, ["id", "type", "condition", "body", "maxIterations"]),
3156
3307
  EndNode: object({ id: string, type: { type: "string", enum: ["end"] } }),
@@ -3166,6 +3317,7 @@ function buildOpenApi(version) {
3166
3317
  }
3167
3318
 
3168
3319
  // src/handlers.ts
3320
+ init_graph();
3169
3321
  init_lanes();
3170
3322
  function jsonResponse(body, status = 200) {
3171
3323
  return new Response(JSON.stringify(body), {
@@ -3310,12 +3462,12 @@ function createRequestHandler(service) {
3310
3462
  return jsonResponse(await service.listRuns({ limit, status }));
3311
3463
  }
3312
3464
  if (path === "/lanes")
3313
- return jsonResponse(await laneInventory());
3465
+ return jsonResponse(await service.laneInventory());
3314
3466
  if (path.startsWith("/lanes/")) {
3315
3467
  const lane = path.slice("/lanes/".length);
3316
3468
  if (!LANE_KINDS2.includes(lane))
3317
3469
  return jsonResponse({ error: "invalid_lane" }, 400);
3318
- return jsonResponse(await probeLane(lane));
3470
+ return jsonResponse(await service.probeLane(lane));
3319
3471
  }
3320
3472
  if (path === "/sessions/list")
3321
3473
  return jsonResponse(service.sessions("list"));