@hasna/workflows 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -1,9 +1,1874 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
19
+
20
+ // src/lanes/types.ts
21
+ import { statSync } from "fs";
22
+ function binaryOnPath(binary) {
23
+ const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
24
+ for (const entry of pathEntries) {
25
+ try {
26
+ const stat = statSync(`${entry}/${binary}`);
27
+ if (stat.isFile())
28
+ return true;
29
+ } catch {}
30
+ }
31
+ return false;
32
+ }
33
+ function withTimeout(promise, timeoutMs, what) {
34
+ return new Promise((resolve, reject) => {
35
+ const timer = setTimeout(() => {
36
+ reject(new Error(`${what} exceeded the ${timeoutMs}ms budget`));
37
+ }, timeoutMs);
38
+ promise.then((value) => {
39
+ clearTimeout(timer);
40
+ resolve(value);
41
+ }, (err) => {
42
+ clearTimeout(timer);
43
+ reject(err);
44
+ });
45
+ });
46
+ }
47
+ function spawnCli(cmd, opts) {
48
+ const started = Date.now();
49
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
50
+ let proc;
51
+ try {
52
+ proc = Bun.spawnSync({
53
+ cmd,
54
+ cwd: opts.cwd,
55
+ env: { ...process.env, ...opts.env ?? {} },
56
+ stdout: "pipe",
57
+ stderr: "pipe",
58
+ timeout: timeoutMs
59
+ });
60
+ } catch (err) {
61
+ const message = String(err instanceof Error ? err.message : err);
62
+ if (message.includes("ENOENT") || /not found/i.test(message)) {
63
+ throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
64
+ }
65
+ return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
66
+ }
67
+ const durationMs = Date.now() - started;
68
+ const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
69
+ const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
70
+ const output = stderr ? `${stdout}
71
+ ${stderr}`.trim() : stdout.trim();
72
+ const exitCode = proc.exitCode ?? 1;
73
+ return { ok: exitCode === 0, exitCode, output, durationMs };
74
+ }
75
+ var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000;
76
+ var init_types = __esm(() => {
77
+ LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
78
+ LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
79
+ constructor(message) {
80
+ super(message);
81
+ this.name = "LaneDependencyMissingError";
82
+ }
83
+ };
84
+ LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
85
+ constructor(message) {
86
+ super(message);
87
+ this.name = "LaneAdapterShapeError";
88
+ }
89
+ };
90
+ });
91
+
92
+ // src/lanes/claude.ts
93
+ function createClaudeAdapter(deps = {}) {
94
+ return {
95
+ kind: "claude",
96
+ async probe() {
97
+ const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
98
+ try {
99
+ await loader();
100
+ return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
101
+ } catch {
102
+ if (binaryOnPath(deps.cliPath ?? "claude")) {
103
+ return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
104
+ }
105
+ return {
106
+ kind: "claude",
107
+ sdk: "@anthropic-ai/claude-agent-sdk",
108
+ wired: false,
109
+ reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
110
+ };
111
+ }
112
+ },
113
+ async run(job) {
114
+ if (!job.prompt) {
115
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
116
+ }
117
+ const started = Date.now();
118
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
119
+ const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
120
+ let sdk;
121
+ try {
122
+ sdk = await loader();
123
+ } catch (err) {
124
+ return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
125
+ }
126
+ const query = sdk.query;
127
+ if (typeof query !== "function") {
128
+ throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
129
+ }
130
+ const prompt = job.prompt;
131
+ try {
132
+ const { output, isError } = await withTimeout((async () => {
133
+ const generator = await query({
134
+ prompt,
135
+ options: { model: job.model, maxTurns: 32, abortSignal: undefined }
136
+ });
137
+ let output2 = "";
138
+ let isError2 = false;
139
+ for await (const message of generator) {
140
+ if (message.type === "assistant") {
141
+ const content = message.message?.content;
142
+ if (Array.isArray(content)) {
143
+ for (const block of content) {
144
+ if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
145
+ output2 += block.text;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ if (message.type === "error")
151
+ isError2 = true;
152
+ }
153
+ return { output: output2, isError: isError2 };
154
+ })(), timeoutMs, `claude lane (${job.prompt.slice(0, 60)})`);
155
+ const durationMs = Date.now() - started;
156
+ return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
157
+ } catch (err) {
158
+ const durationMs = Date.now() - started;
159
+ return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
160
+ }
161
+ }
162
+ };
163
+ }
164
+ function runClaudeCli(job, cliPath, started, timeoutMs) {
165
+ try {
166
+ const result = spawnCli([cliPath, "-p", job.prompt], {
167
+ binaryName: cliPath,
168
+ packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
169
+ timeoutMs,
170
+ env: job.env,
171
+ cwd: job.cwd
172
+ });
173
+ return { ...result, durationMs: Date.now() - started };
174
+ } catch (err) {
175
+ if (err instanceof LaneDependencyMissingError) {
176
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
177
+ }
178
+ throw err;
179
+ }
180
+ }
181
+ var init_claude = __esm(() => {
182
+ init_types();
183
+ });
184
+
185
+ // src/lanes/codex.ts
186
+ function createCodexAdapter(deps = {}) {
187
+ return {
188
+ kind: "codex",
189
+ async probe() {
190
+ const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
191
+ try {
192
+ await loader();
193
+ return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
194
+ } catch {
195
+ if (binaryOnPath(deps.cliPath ?? "codex")) {
196
+ return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
197
+ }
198
+ return {
199
+ kind: "codex",
200
+ sdk: "@openai/codex-sdk",
201
+ wired: false,
202
+ reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
203
+ };
204
+ }
205
+ },
206
+ async run(job) {
207
+ if (!job.prompt) {
208
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
209
+ }
210
+ const started = Date.now();
211
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
212
+ const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
213
+ let sdk;
214
+ try {
215
+ sdk = await loader();
216
+ } catch {
217
+ return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
218
+ }
219
+ const CodexClass = sdk.Codex;
220
+ if (typeof CodexClass !== "function") {
221
+ throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
222
+ }
223
+ try {
224
+ const client = await withTimeout(Promise.resolve(new CodexClass({
225
+ config: { model: job.model },
226
+ env: job.env
227
+ })), timeoutMs, `codex lane (${job.prompt.slice(0, 60)})`);
228
+ const thread = client.startThread?.();
229
+ if (!thread || typeof thread.run !== "function") {
230
+ throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
231
+ }
232
+ const turn = await withTimeout(thread.run(job.prompt), timeoutMs, `codex lane (${job.prompt.slice(0, 60)})`);
233
+ const output = turn?.finalResponse ?? "";
234
+ const durationMs = Date.now() - started;
235
+ return { ok: true, exitCode: 0, output, durationMs };
236
+ } catch (err) {
237
+ const durationMs = Date.now() - started;
238
+ return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
239
+ }
240
+ }
241
+ };
242
+ }
243
+ function runCodexCli(job, cliPath, started, timeoutMs) {
244
+ try {
245
+ const result = spawnCli([cliPath, "exec", "--", job.prompt], {
246
+ binaryName: cliPath,
247
+ packageHint: "@openai/codex-sdk or the codex CLI",
248
+ timeoutMs,
249
+ env: job.env,
250
+ cwd: job.cwd
251
+ });
252
+ return { ...result, durationMs: Date.now() - started };
253
+ } catch (err) {
254
+ if (err instanceof LaneDependencyMissingError) {
255
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
256
+ }
257
+ throw err;
258
+ }
259
+ }
260
+ var init_codex = __esm(() => {
261
+ init_types();
262
+ });
263
+
264
+ // src/lanes/cursor.ts
265
+ function createCursorAdapter(deps = {}) {
266
+ return {
267
+ kind: "cursor",
268
+ async probe() {
269
+ const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
270
+ try {
271
+ await loader();
272
+ return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
273
+ } catch {
274
+ if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
275
+ return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
276
+ }
277
+ return {
278
+ kind: "cursor",
279
+ sdk: "@cursor/sdk",
280
+ wired: false,
281
+ reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
282
+ };
283
+ }
284
+ },
285
+ async run(job) {
286
+ if (!job.prompt) {
287
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
288
+ }
289
+ const started = Date.now();
290
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
291
+ const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
292
+ let sdk;
293
+ try {
294
+ sdk = await loader();
295
+ } catch {
296
+ return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
297
+ }
298
+ try {
299
+ const run = await withTimeout(runLocalAgent(sdk, job), timeoutMs, `cursor lane (${job.prompt.slice(0, 60)})`);
300
+ const durationMs = Date.now() - started;
301
+ const status = run?.status ?? "finished";
302
+ const ok = status === "finished";
303
+ return { ok, exitCode: ok ? 0 : 1, output: run?.result ?? "", durationMs };
304
+ } catch (err) {
305
+ const durationMs = Date.now() - started;
306
+ return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
307
+ }
308
+ }
309
+ };
310
+ }
311
+ async function runLocalAgent(sdk, job) {
312
+ const AgentClass = sdk.Agent;
313
+ const CursorClass = sdk.Cursor;
314
+ if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
315
+ throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
316
+ }
317
+ const agent = typeof AgentClass === "function" ? new AgentClass : undefined;
318
+ if (!agent || typeof agent.send !== "function") {
319
+ throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
320
+ }
321
+ const run = await agent.send(job.prompt, { model: job.model });
322
+ if (!run || typeof run !== "object") {
323
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
324
+ }
325
+ return run;
326
+ }
327
+ function runCursorCli(job, cliPath, started, timeoutMs) {
328
+ try {
329
+ const result = spawnCli([cliPath, "run", job.prompt], {
330
+ binaryName: cliPath,
331
+ packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
332
+ timeoutMs,
333
+ env: job.env,
334
+ cwd: job.cwd
335
+ });
336
+ return { ...result, durationMs: Date.now() - started };
337
+ } catch (err) {
338
+ if (err instanceof LaneDependencyMissingError) {
339
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
340
+ }
341
+ throw err;
342
+ }
343
+ }
344
+ var init_cursor = __esm(() => {
345
+ init_types();
346
+ });
347
+
348
+ // src/lanes/grok.ts
349
+ function createGrokAdapter(deps = {}) {
350
+ return {
351
+ kind: "grok",
352
+ async probe() {
353
+ const cliPath = deps.cliPath ?? "grok";
354
+ if (binaryOnPath(cliPath)) {
355
+ return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
356
+ }
357
+ return {
358
+ kind: "grok",
359
+ sdk: "no npm SDK (measured 2026-08-25)",
360
+ wired: false,
361
+ reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
362
+ };
363
+ },
364
+ async run(job) {
365
+ if (!job.prompt) {
366
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
367
+ }
368
+ const started = Date.now();
369
+ const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
370
+ const cliPath = deps.cliPath ?? "grok";
371
+ try {
372
+ const result = spawnCli([cliPath, job.prompt], {
373
+ binaryName: cliPath,
374
+ packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
375
+ timeoutMs,
376
+ env: job.env,
377
+ cwd: job.cwd
378
+ });
379
+ return { ...result, durationMs: Date.now() - started };
380
+ } catch (err) {
381
+ if (err instanceof LaneDependencyMissingError) {
382
+ return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
383
+ }
384
+ throw err;
385
+ }
386
+ }
387
+ };
388
+ }
389
+ var init_grok = __esm(() => {
390
+ init_types();
391
+ });
392
+
393
+ // src/lanes/index.ts
394
+ var exports_lanes = {};
395
+ __export(exports_lanes, {
396
+ runLaneJob: () => runLaneJob,
397
+ resolveLane: () => resolveLane,
398
+ probeLane: () => probeLane,
399
+ laneInventory: () => laneInventory,
400
+ createGrokAdapter: () => createGrokAdapter,
401
+ createCursorAdapter: () => createCursorAdapter,
402
+ createCodexAdapter: () => createCodexAdapter,
403
+ createClaudeAdapter: () => createClaudeAdapter,
404
+ binaryOnPath: () => binaryOnPath,
405
+ LaneDependencyMissingError: () => LaneDependencyMissingError,
406
+ LaneAdapterShapeError: () => LaneAdapterShapeError,
407
+ LANE_KINDS: () => LANE_KINDS2
408
+ });
409
+ function resolveLane(kind) {
410
+ const adapter = adapters.get(kind);
411
+ if (!adapter)
412
+ throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
413
+ return adapter;
414
+ }
415
+ async function runLaneJob(job) {
416
+ return resolveLane(job.lane).run(job);
417
+ }
418
+ async function probeLane(kind) {
419
+ return resolveLane(kind).probe();
420
+ }
421
+ async function laneInventory() {
422
+ return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
423
+ }
424
+ var adapters;
425
+ var init_lanes = __esm(() => {
426
+ init_claude();
427
+ init_codex();
428
+ init_cursor();
429
+ init_grok();
430
+ init_types();
431
+ init_types();
432
+ init_claude();
433
+ init_codex();
434
+ init_cursor();
435
+ init_grok();
436
+ adapters = new Map([
437
+ ["claude", createClaudeAdapter()],
438
+ ["codex", createCodexAdapter()],
439
+ ["cursor", createCursorAdapter()],
440
+ ["grok", createGrokAdapter()]
441
+ ]);
442
+ });
3
443
 
4
444
  // src/mcp/index.ts
5
445
  import { createInterface } from "readline";
6
446
 
447
+ // src/expr.ts
448
+ class ExprSyntaxError extends Error {
449
+ constructor(message) {
450
+ super(message);
451
+ this.name = "ExprSyntaxError";
452
+ }
453
+ }
454
+
455
+ class ExprEvalError extends Error {
456
+ constructor(message) {
457
+ super(message);
458
+ this.name = "ExprEvalError";
459
+ }
460
+ }
461
+ var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
462
+ function tokenize(input) {
463
+ const tokens = [];
464
+ let i = 0;
465
+ while (i < input.length) {
466
+ const ch = input[i];
467
+ if (ch === " " || ch === "\t" || ch === `
468
+ ` || ch === "\r") {
469
+ i++;
470
+ continue;
471
+ }
472
+ if (ch === "(") {
473
+ tokens.push({ kind: "lparen", value: "(" });
474
+ i++;
475
+ continue;
476
+ }
477
+ if (ch === ")") {
478
+ tokens.push({ kind: "rparen", value: ")" });
479
+ i++;
480
+ continue;
481
+ }
482
+ if (ch === '"' || ch === "'") {
483
+ const quote = ch;
484
+ let j = i + 1;
485
+ let out = "";
486
+ let closed = false;
487
+ while (j < input.length) {
488
+ if (input[j] === quote) {
489
+ closed = true;
490
+ break;
491
+ }
492
+ out += input[j];
493
+ j++;
494
+ }
495
+ if (!closed)
496
+ throw new ExprSyntaxError(`unterminated string literal at offset ${i}`);
497
+ tokens.push({ kind: "string", value: out });
498
+ i = j + 1;
499
+ continue;
500
+ }
501
+ const two = input.slice(i, i + 2);
502
+ if (COMPARISON_OPS.has(two)) {
503
+ tokens.push({ kind: "op", value: two });
504
+ i += 2;
505
+ continue;
506
+ }
507
+ if (ch === "<" || ch === ">" || ch === "=" || ch === "!") {
508
+ tokens.push({ kind: "op", value: ch });
509
+ i++;
510
+ continue;
511
+ }
512
+ if (/[0-9]/.test(ch)) {
513
+ let j = i;
514
+ while (j < input.length && /[0-9.]/.test(input[j]))
515
+ j++;
516
+ tokens.push({ kind: "number", value: input.slice(i, j) });
517
+ i = j;
518
+ continue;
519
+ }
520
+ if (/[A-Za-z_]/.test(ch)) {
521
+ let j = i;
522
+ while (j < input.length && /[A-Za-z0-9_.]/.test(input[j]))
523
+ j++;
524
+ tokens.push({ kind: "ident", value: input.slice(i, j) });
525
+ i = j;
526
+ continue;
527
+ }
528
+ throw new ExprSyntaxError(`unexpected character ${JSON.stringify(ch)} at offset ${i}`);
529
+ }
530
+ return tokens;
531
+ }
532
+
533
+ class Parser {
534
+ tokens;
535
+ pos = 0;
536
+ constructor(tokens) {
537
+ this.tokens = tokens;
538
+ }
539
+ peek() {
540
+ return this.tokens[this.pos];
541
+ }
542
+ next() {
543
+ return this.tokens[this.pos++];
544
+ }
545
+ expectOp(value) {
546
+ const t = this.next();
547
+ if (!t || t.kind !== "op" || t.value !== value) {
548
+ throw new ExprSyntaxError(`expected ${JSON.stringify(value)}`);
549
+ }
550
+ }
551
+ parse() {
552
+ if (this.tokens.length === 0)
553
+ throw new ExprSyntaxError("empty expression");
554
+ const node = this.parseOr();
555
+ if (this.pos < this.tokens.length) {
556
+ throw new ExprSyntaxError(`unexpected trailing token ${JSON.stringify(this.peek().value)}`);
557
+ }
558
+ return node;
559
+ }
560
+ parseOr() {
561
+ let left = this.parseAnd();
562
+ while (this.peek()?.kind === "ident" && this.peek().value === "or") {
563
+ this.next();
564
+ const right = this.parseAnd();
565
+ left = { kind: "or", left, right };
566
+ }
567
+ return left;
568
+ }
569
+ parseAnd() {
570
+ let left = this.parseNot();
571
+ while (this.peek()?.kind === "ident" && this.peek().value === "and") {
572
+ this.next();
573
+ const right = this.parseNot();
574
+ left = { kind: "and", left, right };
575
+ }
576
+ return left;
577
+ }
578
+ parseNot() {
579
+ if (this.peek()?.kind === "ident" && this.peek().value === "not") {
580
+ this.next();
581
+ return { kind: "not", operand: this.parseNot() };
582
+ }
583
+ return this.parseComparison();
584
+ }
585
+ parseComparison() {
586
+ const left = this.parsePrimary();
587
+ const t = this.peek();
588
+ if (t && t.kind === "op" && COMPARISON_OPS.has(t.value)) {
589
+ this.next();
590
+ const right = this.parsePrimary();
591
+ return { kind: "comparison", op: t.value, left, right };
592
+ }
593
+ return left;
594
+ }
595
+ parsePrimary() {
596
+ const t = this.next();
597
+ if (!t)
598
+ throw new ExprSyntaxError("unexpected end of expression");
599
+ if (t.kind === "lparen") {
600
+ const inner = this.parseOr();
601
+ this.expectCloseParen();
602
+ return inner;
603
+ }
604
+ if (t.kind === "number") {
605
+ const value = Number(t.value);
606
+ if (!Number.isFinite(value))
607
+ throw new ExprSyntaxError(`invalid number ${JSON.stringify(t.value)}`);
608
+ return { kind: "literal", value };
609
+ }
610
+ if (t.kind === "string")
611
+ return { kind: "literal", value: t.value };
612
+ if (t.kind === "ident") {
613
+ if (t.value === "true")
614
+ return { kind: "literal", value: true };
615
+ if (t.value === "false")
616
+ return { kind: "literal", value: false };
617
+ return { kind: "path", segments: t.value.split(".") };
618
+ }
619
+ throw new ExprSyntaxError(`unexpected token ${JSON.stringify(t.value)}`);
620
+ }
621
+ expectCloseParen() {
622
+ const t = this.next();
623
+ if (!t || t.kind !== "rparen")
624
+ throw new ExprSyntaxError("expected )");
625
+ }
626
+ }
627
+ function parseExpr(input) {
628
+ return new Parser(tokenize(input)).parse();
629
+ }
630
+ function resolvePath(segments, context) {
631
+ let current = context;
632
+ for (const segment of segments) {
633
+ if (current === null || typeof current !== "object")
634
+ return;
635
+ const record = current;
636
+ if (!(segment in record))
637
+ return;
638
+ current = record[segment];
639
+ }
640
+ if (typeof current === "number" || typeof current === "string" || typeof current === "boolean") {
641
+ return current;
642
+ }
643
+ return;
644
+ }
645
+ function evalNode(node, context) {
646
+ switch (node.kind) {
647
+ case "literal":
648
+ return node.value;
649
+ case "path":
650
+ return resolvePath(node.segments, context);
651
+ case "comparison": {
652
+ const left = evalNode(node.left, context);
653
+ const right = evalNode(node.right, context);
654
+ if (left === undefined || right === undefined) {
655
+ if (node.op === "==")
656
+ return left === undefined && right === undefined;
657
+ if (node.op === "!=")
658
+ return left !== right;
659
+ throw new ExprEvalError("cannot order-compare an unknown path");
660
+ }
661
+ if (typeof left !== typeof right) {
662
+ throw new ExprEvalError(`cannot compare ${JSON.stringify(left)} (${typeof left}) with ${JSON.stringify(right)} (${typeof right})`);
663
+ }
664
+ switch (node.op) {
665
+ case "==":
666
+ return left === right;
667
+ case "!=":
668
+ return left !== right;
669
+ case "<":
670
+ return left < right;
671
+ case "<=":
672
+ return left <= right;
673
+ case ">":
674
+ return left > right;
675
+ case ">=":
676
+ return left >= right;
677
+ default:
678
+ throw new ExprEvalError(`unknown operator ${node.op}`);
679
+ }
680
+ }
681
+ case "and": {
682
+ const left = evalNode(node.left, context);
683
+ const right = evalNode(node.right, context);
684
+ if (typeof left !== "boolean" || typeof right !== "boolean") {
685
+ throw new ExprEvalError("and requires boolean operands");
686
+ }
687
+ return left && right;
688
+ }
689
+ case "or": {
690
+ const left = evalNode(node.left, context);
691
+ const right = evalNode(node.right, context);
692
+ if (typeof left !== "boolean" || typeof right !== "boolean") {
693
+ throw new ExprEvalError("or requires boolean operands");
694
+ }
695
+ return left || right;
696
+ }
697
+ case "not": {
698
+ const operand = evalNode(node.operand, context);
699
+ if (typeof operand !== "boolean")
700
+ throw new ExprEvalError("not requires a boolean operand");
701
+ return !operand;
702
+ }
703
+ }
704
+ }
705
+ function evaluateExpr(input, context) {
706
+ const ast = parseExpr(input);
707
+ const value = evalNode(ast, context);
708
+ if (typeof value !== "boolean") {
709
+ throw new ExprEvalError(`condition must evaluate to a boolean, got ${typeof value}`);
710
+ }
711
+ return value;
712
+ }
713
+
714
+ // src/graph.ts
715
+ var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
716
+ var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
717
+ var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
718
+ function issue(issues, path, message) {
719
+ issues.push({ path, message });
720
+ }
721
+ function findNode(graph, id) {
722
+ return graph.nodes.find((n) => n.id === id);
723
+ }
724
+ function collectEdges(graph) {
725
+ const edges = [];
726
+ for (const node of graph.nodes) {
727
+ switch (node.type) {
728
+ case "start":
729
+ case "step":
730
+ if (node.next)
731
+ edges.push({ from: node.id, to: node.next });
732
+ break;
733
+ case "decision":
734
+ if (node.then)
735
+ edges.push({ from: node.id, to: node.then });
736
+ if (node.else)
737
+ edges.push({ from: node.id, to: node.else });
738
+ break;
739
+ case "while":
740
+ if (node.next)
741
+ edges.push({ from: node.id, to: node.next });
742
+ break;
743
+ case "end":
744
+ break;
745
+ }
746
+ }
747
+ return edges;
748
+ }
749
+ function reachableNodes(graph) {
750
+ const start = graph.nodes.find((n) => n.type === "start");
751
+ if (!start)
752
+ return [];
753
+ const seen = new Set;
754
+ const stack = [start.id];
755
+ while (stack.length > 0) {
756
+ const id = stack.pop();
757
+ if (seen.has(id))
758
+ continue;
759
+ seen.add(id);
760
+ const node = findNode(graph, id);
761
+ if (!node)
762
+ continue;
763
+ if (node.type === "start" || node.type === "step") {
764
+ if (node.next)
765
+ stack.push(node.next);
766
+ } else if (node.type === "decision") {
767
+ if (node.then)
768
+ stack.push(node.then);
769
+ if (node.else)
770
+ stack.push(node.else);
771
+ } else if (node.type === "while") {
772
+ for (const bodyId of node.body)
773
+ stack.push(bodyId);
774
+ if (node.next)
775
+ stack.push(node.next);
776
+ }
777
+ }
778
+ return [...seen];
779
+ }
780
+ function findCycles(graph) {
781
+ const edges = collectEdges(graph);
782
+ const adj = new Map;
783
+ for (const e of edges) {
784
+ const list = adj.get(e.from) ?? [];
785
+ list.push(e.to);
786
+ adj.set(e.from, list);
787
+ }
788
+ const color = new Map;
789
+ const stack = [];
790
+ const cycles = [];
791
+ function visit(id) {
792
+ color.set(id, "grey");
793
+ stack.push(id);
794
+ for (const to of adj.get(id) ?? []) {
795
+ const c = color.get(to);
796
+ if (c === "grey") {
797
+ const idx = stack.indexOf(to);
798
+ cycles.push([...stack.slice(idx), to]);
799
+ } else if (c === undefined) {
800
+ visit(to);
801
+ }
802
+ }
803
+ stack.pop();
804
+ color.set(id, "black");
805
+ }
806
+ for (const node of graph.nodes) {
807
+ if (color.get(node.id) === undefined)
808
+ visit(node.id);
809
+ }
810
+ return cycles;
811
+ }
812
+ function validateGraph(graph) {
813
+ const issues = [];
814
+ if (typeof graph.name !== "string" || graph.name.trim() === "") {
815
+ issue(issues, "name", "graph requires a non-empty name");
816
+ }
817
+ if (typeof graph.version !== "string" || graph.version.trim() === "") {
818
+ issue(issues, "version", "graph requires a version string");
819
+ }
820
+ if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
821
+ issue(issues, "nodes", "graph requires at least one node");
822
+ return { ok: false, issues };
823
+ }
824
+ const starts = graph.nodes.filter((n) => n.type === "start");
825
+ if (starts.length === 0)
826
+ issue(issues, "nodes", "graph requires exactly one start node (found none)");
827
+ if (starts.length > 1)
828
+ issue(issues, "nodes", "graph requires exactly one start node (found multiple)");
829
+ const ends = graph.nodes.filter((n) => n.type === "end");
830
+ if (ends.length === 0)
831
+ issue(issues, "nodes", "graph requires at least one end node");
832
+ const seenIds = new Map;
833
+ for (const node of graph.nodes) {
834
+ if (typeof node.id !== "string" || !NODE_ID_RE.test(node.id)) {
835
+ issue(issues, "nodes", `node id ${JSON.stringify(node.id)} must match ${String(NODE_ID_RE)}`);
836
+ continue;
837
+ }
838
+ if (seenIds.has(node.id)) {
839
+ issue(issues, `nodes.${node.id}`, `duplicate node id ${node.id}`);
840
+ continue;
841
+ }
842
+ seenIds.set(node.id, node);
843
+ if (!GRAPH_NODE_TYPES.includes(node.type)) {
844
+ issue(issues, `nodes.${node.id}`, `unknown node type ${JSON.stringify(node.type)}`);
845
+ continue;
846
+ }
847
+ validateNode(graph, node, issues);
848
+ }
849
+ for (const node of graph.nodes) {
850
+ const edges = nodeEdges(node);
851
+ for (const { kind, target } of edges) {
852
+ if (!seenIds.has(target)) {
853
+ issue(issues, `nodes.${node.id}`, `${kind} edge targets nonexistent node ${target}`);
854
+ }
855
+ }
856
+ }
857
+ if (starts.length === 1) {
858
+ const reachable = new Set(reachableNodes(graph));
859
+ for (const node of graph.nodes) {
860
+ if (!reachable.has(node.id)) {
861
+ issue(issues, `nodes.${node.id}`, `node ${node.id} is unreachable from start`);
862
+ }
863
+ }
864
+ }
865
+ const cycles = findCycles(graph);
866
+ for (const cycle of cycles) {
867
+ issue(issues, "nodes", `explicit cycle detected: ${cycle.join(" -> ")} (loop only via a while node)`);
868
+ }
869
+ return { ok: issues.length === 0, issues };
870
+ }
871
+ function nodeEdges(node) {
872
+ switch (node.type) {
873
+ case "start":
874
+ case "step":
875
+ return node.next ? [{ kind: "next", target: node.next }] : [];
876
+ case "decision":
877
+ return [
878
+ ...node.then ? [{ kind: "then", target: node.then }] : [],
879
+ ...node.else ? [{ kind: "else", target: node.else }] : []
880
+ ];
881
+ case "while":
882
+ return node.next ? [{ kind: "next", target: node.next }] : [];
883
+ case "end":
884
+ return [];
885
+ }
886
+ }
887
+ function validateNode(graph, node, issues) {
888
+ switch (node.type) {
889
+ case "start":
890
+ if (!node.next)
891
+ issue(issues, `nodes.${node.id}`, "start node requires a next edge");
892
+ break;
893
+ case "step": {
894
+ if (!node.prompt && !node.command) {
895
+ issue(issues, `nodes.${node.id}`, "step node requires prompt or command");
896
+ }
897
+ if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
898
+ issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
899
+ }
900
+ if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
901
+ issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
902
+ }
903
+ if (node.timeoutMs !== undefined && (!Number.isInteger(node.timeoutMs) || node.timeoutMs < 1)) {
904
+ issue(issues, `nodes.${node.id}`, "timeoutMs must be a positive integer");
905
+ }
906
+ break;
907
+ }
908
+ case "decision": {
909
+ if (typeof node.condition !== "string" || node.condition.trim() === "") {
910
+ issue(issues, `nodes.${node.id}`, "decision node requires a condition expression");
911
+ } else {
912
+ try {
913
+ parseExpr(node.condition);
914
+ } catch (err) {
915
+ if (err instanceof ExprSyntaxError) {
916
+ issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
917
+ } else {
918
+ throw err;
919
+ }
920
+ }
921
+ }
922
+ if (!node.then && !node.else) {
923
+ issue(issues, `nodes.${node.id}`, "decision node requires then and/or else edge");
924
+ }
925
+ break;
926
+ }
927
+ case "while": {
928
+ if (typeof node.condition !== "string" || node.condition.trim() === "") {
929
+ issue(issues, `nodes.${node.id}`, "while node requires a condition expression");
930
+ } else {
931
+ try {
932
+ parseExpr(node.condition);
933
+ } catch (err) {
934
+ if (err instanceof ExprSyntaxError) {
935
+ issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
936
+ } else {
937
+ throw err;
938
+ }
939
+ }
940
+ }
941
+ if (node.maxIterations === undefined) {
942
+ issue(issues, `nodes.${node.id}`, "while node requires maxIterations (finite loop bound)");
943
+ } else if (!Number.isInteger(node.maxIterations) || node.maxIterations < 1) {
944
+ issue(issues, `nodes.${node.id}`, "maxIterations must be a positive integer");
945
+ }
946
+ if (!Array.isArray(node.body) || node.body.length === 0) {
947
+ issue(issues, `nodes.${node.id}`, "while node requires a non-empty body");
948
+ } else {
949
+ for (const bodyId of node.body) {
950
+ const bodyNode = findNode(graph, bodyId);
951
+ if (!bodyNode) {
952
+ issue(issues, `nodes.${node.id}`, `while body references nonexistent node ${bodyId}`);
953
+ continue;
954
+ }
955
+ if (bodyNode.type === "start" || bodyNode.type === "end" || bodyNode.type === "while") {
956
+ issue(issues, `nodes.${node.id}`, `while body may only contain step/decision nodes in v1 (found ${bodyNode.type})`);
957
+ }
958
+ }
959
+ const bodySet = new Set(node.body);
960
+ for (const bodyId of node.body) {
961
+ const bodyNode = findNode(graph, bodyId);
962
+ if (!bodyNode)
963
+ continue;
964
+ for (const { kind, target } of nodeEdges(bodyNode)) {
965
+ if (!bodySet.has(target)) {
966
+ issue(issues, `nodes.${node.id}`, `while body node ${bodyId} ${kind}-edge leaves the body (target ${target})`);
967
+ }
968
+ }
969
+ }
970
+ }
971
+ break;
972
+ }
973
+ case "end":
974
+ break;
975
+ }
976
+ }
977
+
978
+ // src/daemon.ts
979
+ import { createHash as createHash2 } from "crypto";
980
+
981
+ // src/secrets.ts
982
+ class SecretsGateError extends Error {
983
+ constructor(message) {
984
+ super(message);
985
+ this.name = "SecretsGateError";
986
+ }
987
+ }
988
+ var p = (...parts) => parts.join("");
989
+ var VALUE_PATTERNS = [
990
+ { detector: "anthropic_api_key", re: new RegExp(p("sk-", "ant-") + "[A-Za-z0-9_-]{10,}") },
991
+ { detector: "openai_api_key", re: new RegExp(p("sk-", "proj-") + "[A-Za-z0-9_-]{10,}") },
992
+ { detector: "npm_token", re: /npm_[A-Za-z0-9]{20,}/ },
993
+ { detector: "github_token", re: /gh[pousr]_[A-Za-z0-9]{20,}/ },
994
+ { detector: "aws_access_key", re: /AKIA[0-9A-Z]{16}/ },
995
+ { detector: "google_api_key", re: /AIza[0-9A-Za-z_-]{20,}/ },
996
+ { detector: "xai_api_key", re: new RegExp(p("xai-") + "[A-Za-z0-9]{20,}") },
997
+ { detector: "codex_token", re: new RegExp(p("ctx7", "sk-") + "[A-Za-z0-9]{10,}") },
998
+ { detector: "private_key_block", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/ },
999
+ { detector: "bearer_token", re: /Bearer [A-Za-z0-9._~+/=-]{20,}/ }
1000
+ ];
1001
+ var CREDENTIAL_KEY_RE = /^(token|api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|refresh[_-]?token|session[_-]?token|bot[_-]?token)$/i;
1002
+ function scanString(value, path, findings) {
1003
+ for (const { detector, re } of VALUE_PATTERNS) {
1004
+ if (re.test(value)) {
1005
+ findings.push({ path, detector });
1006
+ return;
1007
+ }
1008
+ }
1009
+ }
1010
+ function isTrivial(value) {
1011
+ const v = value.trim();
1012
+ if (v.length === 0)
1013
+ return true;
1014
+ const upper = v.toUpperCase();
1015
+ if (upper.startsWith("REPLACE") || upper.startsWith("CHANGE") || upper.startsWith("YOUR_") || upper.startsWith("EXAMPLE") || upper.startsWith("PLACEHOLDER"))
1016
+ return true;
1017
+ if (v.length < 8)
1018
+ return true;
1019
+ return false;
1020
+ }
1021
+ function scanForSecrets(value, path = "$") {
1022
+ const findings = [];
1023
+ if (value === null || value === undefined)
1024
+ return findings;
1025
+ if (typeof value === "string") {
1026
+ scanString(value, path, findings);
1027
+ return findings;
1028
+ }
1029
+ if (Array.isArray(value)) {
1030
+ value.forEach((item, index) => {
1031
+ findings.push(...scanForSecrets(item, `${path}[${index}]`));
1032
+ });
1033
+ return findings;
1034
+ }
1035
+ if (typeof value === "object") {
1036
+ const record = value;
1037
+ for (const [key, child] of Object.entries(record)) {
1038
+ if (CREDENTIAL_KEY_RE.test(key) && typeof child === "string" && !isTrivial(child)) {
1039
+ findings.push({ path: `${path}.${key}`, detector: "credential_key_field" });
1040
+ } else {
1041
+ findings.push(...scanForSecrets(child, `${path}.${key}`));
1042
+ }
1043
+ }
1044
+ }
1045
+ return findings;
1046
+ }
1047
+ function assertNoSecrets(value, what) {
1048
+ const findings = scanForSecrets(value);
1049
+ if (findings.length === 0)
1050
+ return;
1051
+ const detail = findings.map((f) => `${f.path} (${f.detector})`).join(", ");
1052
+ throw new SecretsGateError(`secrets write-gate refused to persist ${what}: credential-shaped value at ${detail}`);
1053
+ }
1054
+
1055
+ // src/session.ts
1056
+ import { createHash } from "crypto";
1057
+ var DEFAULT_MAX_ATTEMPTS = 3;
1058
+ function repairTornRuns(store, wal, opts = {}) {
1059
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1060
+ const replay = wal.replay();
1061
+ const liveClaims = replay.liveClaims(opts.now);
1062
+ const report = { interrupted: 0, requeued: 0, failed: 0 };
1063
+ const running = store.listRuns({ status: "running", limit: 1000 });
1064
+ for (const run of running) {
1065
+ if (liveClaims.has(run.id))
1066
+ continue;
1067
+ report.interrupted++;
1068
+ const nodes = store.listRunNodes(run.id);
1069
+ if (run.attempts < maxAttempts) {
1070
+ store.bumpAttempts(run.id);
1071
+ store.setRunStatus(run.id, "interrupted", {
1072
+ error: `torn run interrupted after claim loss (attempt ${run.attempts + 1}/${maxAttempts})`
1073
+ });
1074
+ for (const node of nodes) {
1075
+ if (node.status === "running") {
1076
+ store.setRunNodeStatus(node.id, "pending", { error: undefined });
1077
+ }
1078
+ }
1079
+ report.requeued++;
1080
+ } else {
1081
+ store.setRunStatus(run.id, "failed", {
1082
+ error: `torn run failed after ${maxAttempts} interrupted attempts`
1083
+ });
1084
+ for (const node of nodes) {
1085
+ if (node.status === "running") {
1086
+ store.setRunNodeStatus(node.id, "failed", { error: "run interrupted beyond retry budget" });
1087
+ }
1088
+ }
1089
+ report.failed++;
1090
+ }
1091
+ }
1092
+ return report;
1093
+ }
1094
+ function inputHash(payload) {
1095
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
1096
+ }
1097
+ function memoKey(graphName, nodeId, input) {
1098
+ return `${graphName}:${nodeId}:${inputHash(input)}`;
1099
+ }
1100
+ function tryMemoHit(store, graphName, nodeId, input) {
1101
+ const key = memoKey(graphName, nodeId, input);
1102
+ const memo = store.memoGet(key);
1103
+ if (!memo)
1104
+ return;
1105
+ store.memoHit(key);
1106
+ return memo;
1107
+ }
1108
+ function recordMemo(store, graphName, nodeId, input, output) {
1109
+ const key = memoKey(graphName, nodeId, input);
1110
+ store.memoPut(key, graphName, nodeId, inputHash(input), JSON.stringify(output));
1111
+ }
1112
+ function findRunByIdempotencyKey(store, key) {
1113
+ const runs = store.listRuns({ limit: 1000 });
1114
+ for (const run of runs) {
1115
+ try {
1116
+ const context = JSON.parse(run.contextJson);
1117
+ if (context?.__wf?.idempotencyKey === key)
1118
+ return run;
1119
+ } catch {}
1120
+ }
1121
+ return;
1122
+ }
1123
+
1124
+ // src/daemon.ts
1125
+ init_types();
1126
+ var DEFAULT_LEASE_TTL_MS = 120000;
1127
+ var DEFAULT_MAX_ATTEMPTS2 = 3;
1128
+ var DEFAULT_MAX_DISPATCH = 4;
1129
+ var DEFAULT_MAX_STEPS = 25;
1130
+ var DEFAULT_TIMEOUT_MS = 120000;
1131
+ function nowIso() {
1132
+ return new Date().toISOString();
1133
+ }
1134
+ function stripRunState(context) {
1135
+ const { __wf: _wf, steps: _steps, ...user } = context;
1136
+ return user;
1137
+ }
1138
+ function runCommand(job) {
1139
+ if (!job.command) {
1140
+ return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "no command" };
1141
+ }
1142
+ const started = Date.now();
1143
+ const timeoutMs = job.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1144
+ const proc = Bun.spawnSync({
1145
+ cmd: ["/bin/sh", "-c", job.command],
1146
+ cwd: job.cwd,
1147
+ env: { ...process.env, ...job.env ?? {} },
1148
+ stdout: "pipe",
1149
+ stderr: "pipe",
1150
+ timeout: timeoutMs
1151
+ });
1152
+ const durationMs = Date.now() - started;
1153
+ const stdout = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
1154
+ const stderr = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
1155
+ const output = stderr ? `${stdout}
1156
+ ${stderr}`.trim() : stdout.trim();
1157
+ const exitCode = proc.exitCode ?? (proc.signalCode ? 1 : 1);
1158
+ return { ok: exitCode === 0, exitCode, output, durationMs };
1159
+ }
1160
+
1161
+ class WorkflowsDaemon {
1162
+ store;
1163
+ wal;
1164
+ worker;
1165
+ leaseTtlMs;
1166
+ maxAttempts;
1167
+ maxDispatchPerCycle;
1168
+ maxStepsPerCycle;
1169
+ time;
1170
+ laneRunner;
1171
+ env;
1172
+ leases = new Map;
1173
+ fencingCounter = new Map;
1174
+ graphCache = new Map;
1175
+ isolatedRunId = null;
1176
+ constructor(store, wal, options = {}) {
1177
+ this.store = store;
1178
+ this.wal = wal;
1179
+ this.worker = options.worker ?? "local";
1180
+ this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
1181
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS2;
1182
+ this.maxDispatchPerCycle = options.maxDispatchPerCycle ?? DEFAULT_MAX_DISPATCH;
1183
+ this.maxStepsPerCycle = options.maxStepsPerCycle ?? DEFAULT_MAX_STEPS;
1184
+ this.time = options.time ?? Date.now;
1185
+ this.env = options.env ?? {};
1186
+ this.laneRunner = options.laneRunner ?? defaultLaneRunner;
1187
+ this.isolatedRunId = options.isolatedRunId ?? null;
1188
+ this.replayRegisteredGraphs();
1189
+ }
1190
+ isolateRun(runId) {
1191
+ this.isolatedRunId = runId;
1192
+ }
1193
+ replayRegisteredGraphs() {
1194
+ const entries = this.wal.replay().entries;
1195
+ for (const entry of entries) {
1196
+ const op = entry.op;
1197
+ if (op.op === "graph_registered") {
1198
+ this.graphCache.set(op.name, op.graph);
1199
+ }
1200
+ }
1201
+ }
1202
+ liveLeases() {
1203
+ const now = this.time();
1204
+ for (const [runId, lease] of this.leases) {
1205
+ if (lease.expiresAtMs <= now)
1206
+ this.leases.delete(runId);
1207
+ }
1208
+ return this.leases;
1209
+ }
1210
+ claim(runId) {
1211
+ const now = this.time();
1212
+ const existing = this.leases.get(runId);
1213
+ if (existing && existing.worker !== this.worker && existing.expiresAtMs > now) {
1214
+ return null;
1215
+ }
1216
+ const liveClaims = this.wal.replay().liveClaims(now);
1217
+ const foreign = liveClaims.get(runId);
1218
+ if (foreign && foreign.worker !== this.worker && foreign.expiresAtMs > now) {
1219
+ return null;
1220
+ }
1221
+ const fencing = (this.fencingCounter.get(runId) ?? 0) + 1;
1222
+ this.fencingCounter.set(runId, fencing);
1223
+ const lease = { runId, worker: this.worker, expiresAtMs: now + this.leaseTtlMs, fencing };
1224
+ this.leases.set(runId, lease);
1225
+ this.wal.append({
1226
+ op: "claim_acquired",
1227
+ runId,
1228
+ worker: this.worker,
1229
+ expiresAt: new Date(lease.expiresAtMs).toISOString(),
1230
+ fencing,
1231
+ at: nowIso()
1232
+ });
1233
+ return lease;
1234
+ }
1235
+ heartbeat(runId, fencing) {
1236
+ const lease = this.leases.get(runId);
1237
+ if (!lease || lease.fencing !== fencing)
1238
+ return false;
1239
+ lease.expiresAtMs = this.time() + this.leaseTtlMs;
1240
+ return true;
1241
+ }
1242
+ release(runId, fencing) {
1243
+ const lease = this.leases.get(runId);
1244
+ if (!lease || lease.fencing !== fencing)
1245
+ return false;
1246
+ this.leases.delete(runId);
1247
+ this.wal.append({ op: "claim_released", runId, fencing, at: nowIso() });
1248
+ return true;
1249
+ }
1250
+ async reap() {
1251
+ const report = { expired: 0, interrupted: 0, requeued: 0, failed: 0, dispatched: 0, advanced: 0, completed: 0 };
1252
+ const now = this.time();
1253
+ for (const [runId, lease] of this.leases) {
1254
+ if (lease.expiresAtMs <= now) {
1255
+ this.leases.delete(runId);
1256
+ report.expired++;
1257
+ }
1258
+ }
1259
+ if (this.isolatedRunId === null) {
1260
+ const repair = repairTornRuns(this.store, this.wal, { maxAttempts: this.maxAttempts, now });
1261
+ report.interrupted += repair.interrupted;
1262
+ report.requeued += repair.requeued;
1263
+ report.failed += repair.failed;
1264
+ }
1265
+ const pending = this.store.listRuns({ status: "pending", limit: this.maxDispatchPerCycle }).concat(this.store.listRuns({ status: "interrupted", limit: this.maxDispatchPerCycle })).sort((a, b) => a.createdAt === b.createdAt ? a.id < b.id ? 1 : -1 : a.createdAt < b.createdAt ? 1 : -1).slice(0, this.maxDispatchPerCycle);
1266
+ for (const run of pending) {
1267
+ if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
1268
+ continue;
1269
+ const lease = this.claim(run.id);
1270
+ if (!lease)
1271
+ continue;
1272
+ this.store.setRunStatus(run.id, "running");
1273
+ this.wal.append({ op: "run_started", runId: run.id, at: nowIso() });
1274
+ report.dispatched++;
1275
+ }
1276
+ const running = this.store.listRuns({ status: "running", limit: this.maxStepsPerCycle });
1277
+ let steps = 0;
1278
+ for (const run of running) {
1279
+ if (this.isolatedRunId !== null && run.id !== this.isolatedRunId)
1280
+ continue;
1281
+ if (steps >= this.maxStepsPerCycle)
1282
+ break;
1283
+ const outcome = await this.advanceRun(run.id);
1284
+ steps++;
1285
+ if (outcome.kind === "completed")
1286
+ report.completed++;
1287
+ if (outcome.kind === "failed")
1288
+ report.failed++;
1289
+ if (outcome.kind === "advanced")
1290
+ report.advanced++;
1291
+ }
1292
+ return report;
1293
+ }
1294
+ startRun(graph, context) {
1295
+ const validation = validateGraph(graph);
1296
+ if (!validation.ok) {
1297
+ throw new Error(`graph validation failed: ${validation.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
1298
+ }
1299
+ assertNoSecrets(context, "run context");
1300
+ assertNoSecrets(graph, "graph definition");
1301
+ const existing = this.graphCache.get(graph.name);
1302
+ const serialized = JSON.stringify(graph);
1303
+ if (!existing || JSON.stringify(existing) !== serialized) {
1304
+ this.graphCache.set(graph.name, graph);
1305
+ this.wal.append({ op: "graph_registered", name: graph.name, version: graph.version, graph, at: nowIso() });
1306
+ } else {
1307
+ this.graphCache.set(graph.name, existing);
1308
+ }
1309
+ return this.store.createRun({ graphName: graph.name, graphVersion: graph.version, context: context ?? {} });
1310
+ }
1311
+ async advanceRun(runId) {
1312
+ const run = this.store.getRun(runId);
1313
+ if (!run || run.status !== "running")
1314
+ return { kind: "noop" };
1315
+ const lease = this.leases.get(runId);
1316
+ if (!lease || lease.expiresAtMs <= this.time())
1317
+ return { kind: "blocked" };
1318
+ const graph = this.loadGraph(run.graphName);
1319
+ if (!graph) {
1320
+ this.failRun(run, `graph ${run.graphName} not found in daemon graph cache`);
1321
+ return { kind: "failed", error: "graph not found" };
1322
+ }
1323
+ const context = JSON.parse(run.contextJson);
1324
+ const rawWf = context.__wf ?? {};
1325
+ const wf = {
1326
+ ...rawWf,
1327
+ cursor: typeof rawWf.cursor === "string" ? rawWf.cursor : undefined,
1328
+ loops: rawWf.loops ?? {},
1329
+ completedLoops: rawWf.completedLoops ?? {}
1330
+ };
1331
+ try {
1332
+ const cursor = wf.cursor;
1333
+ const node = cursor === undefined ? graph.nodes.find((n) => n.type === "start") : findNode(graph, cursor);
1334
+ if (!node) {
1335
+ this.failRun(run, `cursor ${cursor ?? "<start>"} resolves to no node`);
1336
+ return { kind: "failed", error: "no node at cursor" };
1337
+ }
1338
+ switch (node.type) {
1339
+ case "start": {
1340
+ wf.cursor = node.next;
1341
+ this.persistContext(run, context, wf);
1342
+ return { kind: "advanced", nodeId: node.id };
1343
+ }
1344
+ case "step": {
1345
+ return await this.executeStep(run, graph, context, wf, node);
1346
+ }
1347
+ case "decision": {
1348
+ const value = evaluateExpr(node.condition, this.conditionScope(context));
1349
+ const next = value ? node.then : node.else;
1350
+ if (!next) {
1351
+ this.failRun(run, `decision ${node.id} evaluated ${value} but no ${value ? "then" : "else"} edge`);
1352
+ return { kind: "failed", error: "missing decision edge" };
1353
+ }
1354
+ wf.cursor = next;
1355
+ this.persistContext(run, context, wf);
1356
+ return { kind: "advanced", nodeId: node.id };
1357
+ }
1358
+ case "while": {
1359
+ return this.executeWhile(run, graph, context, wf, node);
1360
+ }
1361
+ case "end": {
1362
+ this.completeRun(run, context);
1363
+ return { kind: "completed" };
1364
+ }
1365
+ }
1366
+ } catch (err) {
1367
+ if (err instanceof ExprEvalError || err instanceof ExprSyntaxError) {
1368
+ this.failRun(run, `condition error at ${wf.cursor ?? "<start>"}: ${err.message}`);
1369
+ return { kind: "failed", error: err.message };
1370
+ }
1371
+ throw err;
1372
+ }
1373
+ }
1374
+ loadGraph(name) {
1375
+ return this.graphCache.get(name);
1376
+ }
1377
+ conditionScope(context) {
1378
+ return { ...context, steps: context.steps ?? {} };
1379
+ }
1380
+ persistContext(run, context, wf) {
1381
+ context.__wf = wf;
1382
+ this.store.setRunContext(run.id, context);
1383
+ }
1384
+ async executeStep(run, graph, context, wf, node) {
1385
+ const nodeId = node.id;
1386
+ const existing = this.store.listRunNodes(run.id).find((n) => n.nodeId === nodeId);
1387
+ const nodeRow = existing ?? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" });
1388
+ if (node.memo) {
1389
+ const memoInput = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
1390
+ const hit = tryMemoHit(this.store, run.graphName, nodeId, memoInput);
1391
+ if (hit) {
1392
+ const cached = JSON.parse(hit.outputJson);
1393
+ this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: Number(cached.exitCode ?? 0), output: { ...cached, memoHit: true } });
1394
+ this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
1395
+ this.recordStepResult(context, nodeId, { ...cached, memoHit: true });
1396
+ return this.afterNode(run, context, wf, node.next);
1397
+ }
1398
+ }
1399
+ if (nodeRow.status !== "running") {
1400
+ this.store.setRunNodeStatus(nodeRow.id, "running");
1401
+ this.wal.append({ op: "node_started", runId: run.id, nodeId, at: nowIso() });
1402
+ }
1403
+ const job = {
1404
+ lane: node.lane ?? "claude",
1405
+ prompt: node.prompt,
1406
+ command: node.command,
1407
+ timeoutMs: node.timeoutMs,
1408
+ env: this.env
1409
+ };
1410
+ let result;
1411
+ try {
1412
+ result = await this.laneRunner(job);
1413
+ } catch (err) {
1414
+ result = { ok: false, exitCode: 1, output: "", durationMs: 0, error: String(err instanceof Error ? err.message : err) };
1415
+ }
1416
+ const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1417
+ if (!result.ok) {
1418
+ const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1419
+ if (retriesLeft > 0) {
1420
+ this.store.bumpAttemptsNode(nodeRow.id);
1421
+ this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1422
+ return { kind: "advanced", nodeId };
1423
+ }
1424
+ const error = result.error ?? `step ${nodeId} failed with exit ${result.exitCode}`;
1425
+ this.store.setRunNodeStatus(nodeRow.id, "failed", { exitCode: result.exitCode, error });
1426
+ this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
1427
+ this.failRun(run, error);
1428
+ return { kind: "failed", error };
1429
+ }
1430
+ try {
1431
+ assertNoSecrets(output, `node ${nodeId} output`);
1432
+ } catch (err) {
1433
+ const message = err instanceof Error ? err.message : String(err);
1434
+ this.store.setRunNodeStatus(nodeRow.id, "failed", { error: message });
1435
+ this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "failed", at: nowIso() });
1436
+ this.failRun(run, message);
1437
+ return { kind: "failed", error: message };
1438
+ }
1439
+ this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: result.exitCode, output });
1440
+ this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
1441
+ if (node.memo) {
1442
+ recordMemo(this.store, run.graphName, nodeId, { command: node.command, prompt: node.prompt, context: stripRunState(context) }, output);
1443
+ this.wal.append({ op: "memo_set", key: `${run.graphName}:${nodeId}:${createHash2("sha256").update(JSON.stringify({ command: node.command, prompt: node.prompt, context: stripRunState(context) })).digest("hex")}`, at: nowIso() });
1444
+ }
1445
+ this.recordStepResult(context, nodeId, output);
1446
+ return this.afterNode(run, context, wf, node.next);
1447
+ }
1448
+ afterNode(run, context, wf, next) {
1449
+ if (next) {
1450
+ wf.cursor = next;
1451
+ this.persistContext(run, context, wf);
1452
+ return { kind: "advanced", nodeId: wf.cursor };
1453
+ }
1454
+ const cursor = wf.cursor;
1455
+ const graph = this.loadGraph(run.graphName);
1456
+ const whileNode = graph.nodes.find((n) => n.type === "while" && n.body.includes(cursor ?? ""));
1457
+ if (whileNode) {
1458
+ const index = whileNode.body.indexOf(cursor ?? "");
1459
+ if (index >= 0 && index < whileNode.body.length - 1) {
1460
+ wf.cursor = whileNode.body[index + 1];
1461
+ this.persistContext(run, context, wf);
1462
+ return { kind: "advanced", nodeId: wf.cursor };
1463
+ }
1464
+ wf.loops[whileNode.id] = (wf.loops[whileNode.id] ?? 0) + 1;
1465
+ wf.cursor = whileNode.id;
1466
+ this.persistContext(run, context, wf);
1467
+ return { kind: "advanced", nodeId: whileNode.id };
1468
+ }
1469
+ this.completeRun(run, context);
1470
+ return { kind: "completed" };
1471
+ }
1472
+ executeWhile(run, graph, context, wf, node) {
1473
+ const iteration = wf.loops[node.id] ?? 0;
1474
+ if (iteration >= node.maxIterations) {
1475
+ const error = `while node ${node.id} exceeded maxIterations (${node.maxIterations})`;
1476
+ this.failRun(run, error);
1477
+ return { kind: "failed", error };
1478
+ }
1479
+ const scope = { ...this.conditionScope(context), i: iteration };
1480
+ const keepGoing = evaluateExpr(node.condition, scope);
1481
+ if (!keepGoing) {
1482
+ wf.completedLoops[node.id] = iteration;
1483
+ delete wf.loops[node.id];
1484
+ wf.cursor = node.next;
1485
+ this.persistContext(run, context, wf);
1486
+ return { kind: "advanced", nodeId: node.id };
1487
+ }
1488
+ wf.cursor = node.body[0];
1489
+ this.persistContext(run, context, wf);
1490
+ return { kind: "advanced", nodeId: node.id };
1491
+ }
1492
+ recordStepResult(context, nodeId, output) {
1493
+ const steps = context.steps ?? {};
1494
+ steps[nodeId] = output;
1495
+ context.steps = steps;
1496
+ }
1497
+ completeRun(run, context) {
1498
+ const result = {
1499
+ steps: context.steps ?? {},
1500
+ iterations: context.__wf?.completedLoops ?? {}
1501
+ };
1502
+ try {
1503
+ assertNoSecrets(result, "run result");
1504
+ } catch (err) {
1505
+ const message = err instanceof Error ? err.message : String(err);
1506
+ this.failRun(run, message);
1507
+ return;
1508
+ }
1509
+ this.store.setRunStatus(run.id, "completed", { result, finishedAt: nowIso(), error: null });
1510
+ this.wal.append({ op: "run_finished", runId: run.id, status: "completed", at: nowIso() });
1511
+ const lease = this.leases.get(run.id);
1512
+ if (lease)
1513
+ this.release(run.id, lease.fencing);
1514
+ }
1515
+ failRun(run, error) {
1516
+ this.store.setRunStatus(run.id, "failed", { error, finishedAt: nowIso() });
1517
+ this.wal.append({ op: "run_finished", runId: run.id, status: "failed", at: nowIso() });
1518
+ const lease = this.leases.get(run.id);
1519
+ if (lease)
1520
+ this.release(run.id, lease.fencing);
1521
+ }
1522
+ }
1523
+ var defaultLaneRunner = async (job) => {
1524
+ if (job.command)
1525
+ return runCommand(job);
1526
+ const { runLaneJob: runLaneJob2 } = await Promise.resolve().then(() => (init_lanes(), exports_lanes));
1527
+ return runLaneJob2(job);
1528
+ };
1529
+ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1530
+ const daemon = new WorkflowsDaemon(store, wal, {
1531
+ worker: "local",
1532
+ laneRunner: options.laneRunner,
1533
+ time: options.time,
1534
+ env: options.env,
1535
+ isolatedRunId: options.isolatedRunId
1536
+ });
1537
+ const run = daemon.startRun(graph, context);
1538
+ daemon.isolateRun(run.id);
1539
+ const maxCycles = options.maxCycles ?? 500;
1540
+ for (let cycle = 0;cycle < maxCycles; cycle++) {
1541
+ const report = await daemon.reap();
1542
+ const current = store.getRun(run.id);
1543
+ if (current.status !== "running")
1544
+ return current;
1545
+ if (report.advanced === 0 && report.dispatched === 0) {
1546
+ return store.getRun(run.id);
1547
+ }
1548
+ }
1549
+ return store.getRun(run.id);
1550
+ }
1551
+
1552
+ // src/agent-tools.ts
1553
+ init_lanes();
1554
+
1555
+ // src/store.ts
1556
+ import { Database } from "bun:sqlite";
1557
+ import { mkdirSync } from "fs";
1558
+ import { join } from "path";
1559
+ import { randomUUID } from "crypto";
1560
+ function nowIso2() {
1561
+ return new Date().toISOString();
1562
+ }
1563
+ function openStore(dataDir) {
1564
+ mkdirSync(dataDir, { recursive: true });
1565
+ const db = new Database(join(dataDir, "workflows.db"));
1566
+ db.exec("PRAGMA journal_mode = WAL");
1567
+ db.exec("PRAGMA foreign_keys = ON");
1568
+ db.exec(`
1569
+ CREATE TABLE IF NOT EXISTS runs (
1570
+ id TEXT PRIMARY KEY,
1571
+ graph_name TEXT NOT NULL,
1572
+ graph_version TEXT NOT NULL,
1573
+ status TEXT NOT NULL,
1574
+ attempts INTEGER NOT NULL DEFAULT 0,
1575
+ context_json TEXT NOT NULL,
1576
+ result_json TEXT,
1577
+ error TEXT,
1578
+ started_at TEXT,
1579
+ finished_at TEXT,
1580
+ created_at TEXT NOT NULL,
1581
+ updated_at TEXT NOT NULL
1582
+ );
1583
+ CREATE TABLE IF NOT EXISTS run_nodes (
1584
+ id TEXT PRIMARY KEY,
1585
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
1586
+ node_id TEXT NOT NULL,
1587
+ status TEXT NOT NULL,
1588
+ attempts INTEGER NOT NULL DEFAULT 0,
1589
+ lane TEXT,
1590
+ exit_code INTEGER,
1591
+ output_json TEXT,
1592
+ error TEXT,
1593
+ started_at TEXT,
1594
+ finished_at TEXT
1595
+ );
1596
+ CREATE TABLE IF NOT EXISTS memos (
1597
+ key TEXT PRIMARY KEY,
1598
+ graph_name TEXT NOT NULL,
1599
+ node_id TEXT NOT NULL,
1600
+ input_hash TEXT NOT NULL,
1601
+ output_json TEXT NOT NULL,
1602
+ hit_count INTEGER NOT NULL DEFAULT 0,
1603
+ created_at TEXT NOT NULL
1604
+ );
1605
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
1606
+ CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
1607
+ CREATE INDEX IF NOT EXISTS idx_run_nodes_run ON run_nodes(run_id);
1608
+ `);
1609
+ const rowToRun = (r) => ({
1610
+ id: String(r.id),
1611
+ graphName: String(r.graph_name),
1612
+ graphVersion: String(r.graph_version),
1613
+ status: r.status,
1614
+ attempts: Number(r.attempts),
1615
+ contextJson: String(r.context_json),
1616
+ resultJson: r.result_json === null ? null : String(r.result_json),
1617
+ error: r.error === null ? null : String(r.error),
1618
+ startedAt: r.started_at === null ? null : String(r.started_at),
1619
+ finishedAt: r.finished_at === null ? null : String(r.finished_at),
1620
+ createdAt: String(r.created_at),
1621
+ updatedAt: String(r.updated_at)
1622
+ });
1623
+ const rowToNode = (r) => ({
1624
+ id: String(r.id),
1625
+ runId: String(r.run_id),
1626
+ nodeId: String(r.node_id),
1627
+ status: r.status,
1628
+ attempts: Number(r.attempts),
1629
+ lane: r.lane === null ? null : String(r.lane),
1630
+ exitCode: r.exit_code === null ? null : Number(r.exit_code),
1631
+ outputJson: r.output_json === null ? null : String(r.output_json),
1632
+ error: r.error === null ? null : String(r.error),
1633
+ startedAt: r.started_at === null ? null : String(r.started_at),
1634
+ finishedAt: r.finished_at === null ? null : String(r.finished_at)
1635
+ });
1636
+ const rowToMemo = (r) => ({
1637
+ key: String(r.key),
1638
+ graphName: String(r.graph_name),
1639
+ nodeId: String(r.node_id),
1640
+ inputHash: String(r.input_hash),
1641
+ outputJson: String(r.output_json),
1642
+ hitCount: Number(r.hit_count),
1643
+ createdAt: String(r.created_at)
1644
+ });
1645
+ return {
1646
+ close() {
1647
+ db.close();
1648
+ },
1649
+ listTables() {
1650
+ const rows = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
1651
+ return rows.map((r) => r.name);
1652
+ },
1653
+ createRun(input) {
1654
+ const id = randomUUID();
1655
+ const now = nowIso2();
1656
+ db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
1657
+ VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now);
1658
+ return this.getRun(id);
1659
+ },
1660
+ getRun(id) {
1661
+ const row = db.query(`SELECT * FROM runs WHERE id = ?`).get(id);
1662
+ return row ? rowToRun(row) : undefined;
1663
+ },
1664
+ listRuns(opts = {}) {
1665
+ const limit = Math.min(opts.limit ?? 100, 1000);
1666
+ if (opts.status) {
1667
+ const rows2 = db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(opts.status, limit);
1668
+ return rows2.map((r) => rowToRun(r));
1669
+ }
1670
+ const rows = db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit);
1671
+ return rows.map((r) => rowToRun(r));
1672
+ },
1673
+ setRunStatus(id, status, patch = {}) {
1674
+ const now = nowIso2();
1675
+ const existing = this.getRun(id);
1676
+ const resultJson = patch.result !== undefined ? JSON.stringify(patch.result) : existing?.resultJson ?? null;
1677
+ const error = patch.error !== undefined ? patch.error : existing?.error ?? null;
1678
+ const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing?.startedAt === null ? now : existing?.startedAt ?? null;
1679
+ const terminal = status === "completed" || status === "failed" || status === "cancelled" || status === "interrupted";
1680
+ const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing?.finishedAt === null ? now : existing?.finishedAt ?? null;
1681
+ db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id);
1682
+ },
1683
+ bumpAttempts(id) {
1684
+ db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id);
1685
+ },
1686
+ setRunContext(id, context) {
1687
+ db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id);
1688
+ },
1689
+ createRunNode(input) {
1690
+ const id = randomUUID();
1691
+ db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
1692
+ VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null);
1693
+ return this.getRunNode(id);
1694
+ },
1695
+ getRunNode(id) {
1696
+ const row = db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id);
1697
+ return row ? rowToNode(row) : undefined;
1698
+ },
1699
+ listRunNodes(runId) {
1700
+ const rows = db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId);
1701
+ return rows.map((r) => rowToNode(r));
1702
+ },
1703
+ setRunNodeStatus(id, status, patch = {}) {
1704
+ const now = nowIso2();
1705
+ const existing = this.getRunNode(id);
1706
+ if (!existing)
1707
+ return;
1708
+ const exitCode = patch.exitCode !== undefined ? patch.exitCode : existing.exitCode;
1709
+ const outputJson = patch.output !== undefined ? JSON.stringify(patch.output) : existing.outputJson;
1710
+ const error = patch.error !== undefined ? patch.error : existing.error;
1711
+ const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing.startedAt === null ? now : existing.startedAt;
1712
+ const terminal = status === "completed" || status === "failed" || status === "skipped";
1713
+ const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing.finishedAt === null ? now : existing.finishedAt;
1714
+ db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id);
1715
+ },
1716
+ bumpAttemptsNode(id) {
1717
+ db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id);
1718
+ },
1719
+ memoPut(key, graphName, nodeId, inputHash2, outputJson) {
1720
+ db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
1721
+ VALUES (?, ?, ?, ?, ?, 0, ?)
1722
+ ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2());
1723
+ },
1724
+ memoGet(key) {
1725
+ const row = db.query(`SELECT * FROM memos WHERE key = ?`).get(key);
1726
+ return row ? rowToMemo(row) : undefined;
1727
+ },
1728
+ memoHit(key) {
1729
+ db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key);
1730
+ },
1731
+ memoList() {
1732
+ const rows = db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all();
1733
+ return rows.map((r) => rowToMemo(r));
1734
+ },
1735
+ memoClear() {
1736
+ db.exec(`DELETE FROM memos`);
1737
+ }
1738
+ };
1739
+ }
1740
+
1741
+ // src/wal.ts
1742
+ import { createHash as createHash3 } from "crypto";
1743
+ import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1744
+ import { join as join2 } from "path";
1745
+ var WAL_FILE_NAME = "session.wal";
1746
+ var SESSIONS_DIR_NAME = "sessions";
1747
+ var MAX_WAL_BYTES = 256 * 1024 * 1024;
1748
+ function checksumOf(op) {
1749
+ if (op === undefined || op === null)
1750
+ return "";
1751
+ return createHash3("sha256").update(JSON.stringify(op)).digest("hex");
1752
+ }
1753
+
1754
+ class SessionWAL {
1755
+ filePath;
1756
+ constructor(filePath) {
1757
+ this.filePath = filePath;
1758
+ }
1759
+ static open(dataDir) {
1760
+ mkdirSync2(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1761
+ const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1762
+ const legacy = join2(dataDir, WAL_FILE_NAME);
1763
+ if (existsSync(legacy) && !existsSync(canonical)) {
1764
+ try {
1765
+ renameSync(legacy, canonical);
1766
+ } catch {
1767
+ return new SessionWAL(legacy);
1768
+ }
1769
+ }
1770
+ return new SessionWAL(canonical);
1771
+ }
1772
+ static openAt(filePath) {
1773
+ return new SessionWAL(filePath);
1774
+ }
1775
+ append(op) {
1776
+ const at = op.at ?? new Date().toISOString();
1777
+ const full = { ...op, at };
1778
+ const nextSeq = this.nextSeq();
1779
+ const entry = { seq: nextSeq, at, checksum: checksumOf(full), op: full };
1780
+ appendFileSync(this.filePath, JSON.stringify(entry) + `
1781
+ `, "utf8");
1782
+ return entry;
1783
+ }
1784
+ nextSeq() {
1785
+ const replay = this.replay();
1786
+ return replay.entries.length + 1;
1787
+ }
1788
+ replay() {
1789
+ const entries = [];
1790
+ let torn = false;
1791
+ let repaired = false;
1792
+ let content;
1793
+ try {
1794
+ content = readFileSync(this.filePath, "utf8");
1795
+ } catch {
1796
+ return { entries, torn, repaired, liveClaims: () => new Map };
1797
+ }
1798
+ if (content.length > MAX_WAL_BYTES) {
1799
+ throw new Error(`session WAL exceeds ${MAX_WAL_BYTES} bytes; refusing to replay a truncated journal`);
1800
+ }
1801
+ if (content === "") {
1802
+ return { entries, torn, repaired, liveClaims: () => new Map };
1803
+ }
1804
+ const lines = content.split(`
1805
+ `);
1806
+ let badOffset = null;
1807
+ let expectedSeq = 1;
1808
+ for (let i = 0;i < lines.length; i++) {
1809
+ const line = lines[i];
1810
+ if (line === "")
1811
+ continue;
1812
+ if (badOffset !== null)
1813
+ break;
1814
+ const lineOffset = cumulativeOffset(lines, i);
1815
+ let entry;
1816
+ try {
1817
+ entry = JSON.parse(line);
1818
+ } catch {
1819
+ badOffset = lineOffset;
1820
+ break;
1821
+ }
1822
+ if (typeof entry?.seq !== "number" || entry.seq !== expectedSeq || entry.checksum !== checksumOf(entry.op)) {
1823
+ badOffset = lineOffset;
1824
+ break;
1825
+ }
1826
+ entries.push(entry);
1827
+ expectedSeq++;
1828
+ }
1829
+ if (badOffset !== null) {
1830
+ torn = true;
1831
+ try {
1832
+ truncateSync(this.filePath, badOffset);
1833
+ repaired = true;
1834
+ } catch {}
1835
+ }
1836
+ return {
1837
+ entries,
1838
+ torn,
1839
+ repaired,
1840
+ liveClaims: (nowMs) => reconstructLiveClaims(entries, nowMs)
1841
+ };
1842
+ }
1843
+ close() {}
1844
+ }
1845
+ function cumulativeOffset(lines, index) {
1846
+ let offset = 0;
1847
+ for (let i = 0;i < index; i++) {
1848
+ offset += Buffer.byteLength(lines[i]) + 1;
1849
+ }
1850
+ return offset;
1851
+ }
1852
+ function reconstructLiveClaims(entries, nowMs = Date.now()) {
1853
+ const claims = new Map;
1854
+ for (const entry of entries) {
1855
+ const op = entry.op;
1856
+ if (op.op === "claim_acquired") {
1857
+ claims.set(op.runId, { worker: op.worker, fencing: op.fencing, expiresAtMs: Date.parse(op.expiresAt) });
1858
+ } else if (op.op === "claim_released") {
1859
+ const existing = claims.get(op.runId);
1860
+ if (existing && existing.fencing === op.fencing) {
1861
+ claims.delete(op.runId);
1862
+ }
1863
+ }
1864
+ }
1865
+ for (const [runId, claim] of claims) {
1866
+ if (claim.expiresAtMs <= nowMs)
1867
+ claims.delete(runId);
1868
+ }
1869
+ return claims;
1870
+ }
1871
+
7
1872
  // src/agent-tools.ts
8
1873
  function textResult(payload) {
9
1874
  return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: false };
@@ -24,10 +1889,47 @@ function workflowsTools(_service) {
24
1889
  name: "workflows_ready",
25
1890
  description: "Report the workflows service readiness (version check).",
26
1891
  inputSchema: { type: "object", properties: {} }
1892
+ },
1893
+ {
1894
+ name: "workflows_validate",
1895
+ description: "Validate a workflow graph (JSON object with name, version, nodes).",
1896
+ inputSchema: {
1897
+ type: "object",
1898
+ properties: { graph: { type: "object", description: "the workflow graph" } },
1899
+ required: ["graph"]
1900
+ }
1901
+ },
1902
+ {
1903
+ name: "workflows_run",
1904
+ description: "Run a workflow graph to a terminal state (bounded cycles; command steps execute; lane steps need the lane substrate).",
1905
+ inputSchema: {
1906
+ type: "object",
1907
+ properties: {
1908
+ graph: { type: "object", description: "the workflow graph" },
1909
+ context: { type: "object", description: "optional run context" }
1910
+ },
1911
+ required: ["graph"]
1912
+ }
1913
+ },
1914
+ {
1915
+ name: "workflows_runs_list",
1916
+ description: "List recent runs from the local store.",
1917
+ inputSchema: {
1918
+ type: "object",
1919
+ properties: {
1920
+ status: { type: "string", description: "optional status filter" },
1921
+ limit: { type: "number", description: "max rows (default 100)" }
1922
+ }
1923
+ }
1924
+ },
1925
+ {
1926
+ name: "workflows_lanes_list",
1927
+ description: "List the four lane adapters with their wired-vs-not-ready-with-reason registry shape.",
1928
+ inputSchema: { type: "object", properties: {} }
27
1929
  }
28
1930
  ];
29
1931
  }
30
- function callWorkflowTool(service, name, _args) {
1932
+ async function callWorkflowTool(service, name, args) {
31
1933
  switch (name) {
32
1934
  case "workflows_version":
33
1935
  return textResult({ service: service.name, version: service.version });
@@ -35,18 +1937,51 @@ function callWorkflowTool(service, name, _args) {
35
1937
  return textResult(service.health());
36
1938
  case "workflows_ready":
37
1939
  return textResult(service.ready());
1940
+ case "workflows_validate": {
1941
+ const graph = args.graph;
1942
+ const result = validateGraph(graph);
1943
+ return textResult(result);
1944
+ }
1945
+ case "workflows_run": {
1946
+ const graph = args.graph;
1947
+ const store = openStore(service.config.dataDir);
1948
+ const wal = SessionWAL.open(service.config.dataDir);
1949
+ try {
1950
+ const final = await runGraphToCompletion(store, wal, graph, args.context ?? {});
1951
+ return textResult({
1952
+ runId: final.id,
1953
+ status: final.status,
1954
+ error: final.error ?? null,
1955
+ result: final.resultJson ? JSON.parse(final.resultJson) : null
1956
+ });
1957
+ } finally {
1958
+ store.close();
1959
+ }
1960
+ }
1961
+ case "workflows_runs_list": {
1962
+ const store = openStore(service.config.dataDir);
1963
+ try {
1964
+ const status = typeof args.status === "string" ? args.status : undefined;
1965
+ const limit = typeof args.limit === "number" ? args.limit : 100;
1966
+ return textResult(store.listRuns({ status, limit }));
1967
+ } finally {
1968
+ store.close();
1969
+ }
1970
+ }
1971
+ case "workflows_lanes_list":
1972
+ return textResult(await laneInventory());
38
1973
  default:
39
1974
  throw new Error(`Unknown tool: ${name}`);
40
1975
  }
41
1976
  }
42
1977
 
43
1978
  // src/service.ts
44
- import { join } from "path";
1979
+ import { join as join3 } from "path";
45
1980
  // package.json
46
1981
  var package_default = {
47
1982
  name: "@hasna/workflows",
48
- version: "0.1.0",
49
- description: "Universal graph workflow app \u2014 scaffold with version/health/readiness on all four surfaces; graph language, store, session WAL, daemon, and lane adapters are roadmap",
1983
+ version: "0.1.1",
1984
+ 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",
50
1985
  type: "module",
51
1986
  main: "./dist/index.js",
52
1987
  types: "./types/index.d.ts",
@@ -116,10 +2051,13 @@ var package_default = {
116
2051
  author: "Hasna",
117
2052
  license: "Apache-2.0",
118
2053
  dependencies: {
2054
+ "@anthropic-ai/claude-agent-sdk": "^0.3.234",
2055
+ "@cursor/sdk": "^1.0.28",
2056
+ "@openai/codex-sdk": "^0.147.0",
119
2057
  commander: "^13.1.0"
120
2058
  },
121
2059
  devDependencies: {
122
- "@hasna/contracts": "0.14.0",
2060
+ "@hasna/contracts": "0.14.1",
123
2061
  "@types/bun": "latest",
124
2062
  "@types/pg": "^8.15.6",
125
2063
  pg: "^8.16.3",
@@ -153,7 +2091,7 @@ function resolveWorkflowsConfig(overrides = {}) {
153
2091
  return {
154
2092
  port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
155
2093
  host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
156
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join(home, ".hasna", "workflows"),
2094
+ dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join3(home, ".hasna", "workflows"),
157
2095
  apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
158
2096
  apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
159
2097
  };
@@ -185,6 +2123,45 @@ class WorkflowsService {
185
2123
  checks: { version: "ok" }
186
2124
  };
187
2125
  }
2126
+ async triggerRun(graph, context = {}, opts = {}) {
2127
+ const store = openStore(this.config.dataDir);
2128
+ const wal = SessionWAL.open(this.config.dataDir);
2129
+ try {
2130
+ if (opts.idempotencyKey) {
2131
+ const existing = findRunByIdempotencyKey(store, opts.idempotencyKey);
2132
+ if (existing) {
2133
+ return {
2134
+ runId: existing.id,
2135
+ status: existing.status,
2136
+ error: existing.error ?? null,
2137
+ result: existing.resultJson ? JSON.parse(existing.resultJson) : null,
2138
+ reused: true
2139
+ };
2140
+ }
2141
+ }
2142
+ const contextWithKey = opts.idempotencyKey ? {
2143
+ ...context,
2144
+ __wf: {
2145
+ cursor: undefined,
2146
+ loops: {},
2147
+ completedLoops: {},
2148
+ ...context?.__wf ?? {},
2149
+ idempotencyKey: opts.idempotencyKey
2150
+ }
2151
+ } : context;
2152
+ const maxCycles = Math.min(Math.max(opts.maxCycles ?? 500, 1), 2000);
2153
+ const final = await runGraphToCompletion(store, wal, graph, contextWithKey, { maxCycles });
2154
+ return {
2155
+ runId: final.id,
2156
+ status: final.status,
2157
+ error: final.error ?? null,
2158
+ result: final.resultJson ? JSON.parse(final.resultJson) : null,
2159
+ reused: false
2160
+ };
2161
+ } finally {
2162
+ store.close();
2163
+ }
2164
+ }
188
2165
  }
189
2166
  function createWorkflowsService(config = {}) {
190
2167
  return new WorkflowsService(config);
@@ -256,14 +2233,16 @@ rl.on("line", (line) => {
256
2233
  const params = msg.params ?? {};
257
2234
  const name = typeof params.name === "string" ? params.name : "";
258
2235
  const args = params.arguments ?? {};
259
- try {
260
- respond(msg.id ?? null, callWorkflowTool(service, name, args));
261
- } catch (err) {
262
- respond(msg.id ?? null, {
263
- content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }],
264
- isError: true
265
- });
266
- }
2236
+ (async () => {
2237
+ try {
2238
+ respond(msg.id ?? null, await callWorkflowTool(service, name, args));
2239
+ } catch (err) {
2240
+ respond(msg.id ?? null, {
2241
+ content: [{ type: "text", text: String(err instanceof Error ? err.message : err) }],
2242
+ isError: true
2243
+ });
2244
+ }
2245
+ })();
267
2246
  return;
268
2247
  }
269
2248
  respondError(msg.id ?? null, -32601, `Method not found: ${method}`);