@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/README.md +53 -0
- package/dist/agent-tools.js +36 -500
- package/dist/cli/index.js +23 -12
- package/dist/handlers.js +1226 -1074
- package/dist/index.js +1228 -1076
- package/dist/mcp/index.js +15 -4
- package/dist/sdk.js +4 -4
- package/dist/serve/admin.js +188 -43
- package/dist/serve/index.js +229 -43
- package/dist/service.js +1223 -1072
- package/hasna.contract.json +1 -1
- package/package.json +3 -3
- package/types/graph.d.ts +6 -0
- package/types/journal-directory.d.ts +4 -0
- package/types/lanes/codex.d.ts +0 -6
- package/types/lanes/types.d.ts +4 -0
- package/types/sdk/generated-api.d.ts +2 -0
- package/types/serve/journal-lease.d.ts +7 -0
- package/types/service.d.ts +12 -4
- package/types/types.d.ts +4 -0
package/dist/service.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/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
90
|
+
|
|
91
|
+
class Parser {
|
|
92
|
+
tokens;
|
|
93
|
+
pos = 0;
|
|
94
|
+
constructor(tokens) {
|
|
95
|
+
this.tokens = tokens;
|
|
355
96
|
}
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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");
|
|
244
|
+
}
|
|
245
|
+
return left && right;
|
|
246
|
+
}
|
|
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");
|
|
441
252
|
}
|
|
253
|
+
return left || right;
|
|
442
254
|
}
|
|
443
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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/
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
475
|
-
return
|
|
292
|
+
function findNode(graph, id) {
|
|
293
|
+
return graph.nodes.find((n) => n.id === id);
|
|
476
294
|
}
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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 (
|
|
662
|
-
|
|
663
|
-
|
|
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 (
|
|
667
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
733
|
-
if (
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
|
668
|
+
return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
|
|
748
669
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
693
|
+
};
|
|
694
|
+
LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
|
|
695
|
+
constructor(message) {
|
|
696
|
+
super(message);
|
|
697
|
+
this.name = "LaneAdapterShapeError";
|
|
763
698
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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");
|
|
814
|
-
}
|
|
815
|
-
if (typeof left !== typeof right) {
|
|
816
|
-
throw new ExprEvalError(`cannot compare ${JSON.stringify(left)} (${typeof left}) with ${JSON.stringify(right)} (${typeof right})`);
|
|
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
|
+
};
|
|
817
727
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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}`);
|
|
728
|
+
},
|
|
729
|
+
async run(job) {
|
|
730
|
+
if (!job.prompt) {
|
|
731
|
+
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
|
|
833
732
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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);
|
|
840
741
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
742
|
+
const query = sdk.query;
|
|
743
|
+
if (typeof query !== "function") {
|
|
744
|
+
throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
|
|
745
|
+
}
|
|
746
|
+
const prompt = job.prompt;
|
|
747
|
+
const abortController = new AbortController;
|
|
748
|
+
try {
|
|
749
|
+
const { output, isError } = await withTimeout((async () => {
|
|
750
|
+
const generator = await query({
|
|
751
|
+
prompt,
|
|
752
|
+
options: { model: job.model, cwd: job.cwd, maxTurns: 32, abortController, env: subscriptionSdkEnvironment(job.env) }
|
|
753
|
+
});
|
|
754
|
+
let output2 = "";
|
|
755
|
+
let isError2 = false;
|
|
756
|
+
for await (const message of generator) {
|
|
757
|
+
if (message.type === "assistant") {
|
|
758
|
+
const content = message.message?.content;
|
|
759
|
+
if (Array.isArray(content)) {
|
|
760
|
+
for (const block of content) {
|
|
761
|
+
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
|
762
|
+
output2 += block.text;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (message.type === "error")
|
|
768
|
+
isError2 = true;
|
|
769
|
+
}
|
|
770
|
+
return { output: output2, isError: isError2 };
|
|
771
|
+
})(), timeoutMs, "claude lane", () => abortController.abort());
|
|
772
|
+
const durationMs = Date.now() - started;
|
|
773
|
+
return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
|
|
774
|
+
} catch (err) {
|
|
775
|
+
const durationMs = Date.now() - started;
|
|
776
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
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
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
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 {
|
|
987
|
+
return { status: candidate.status, result: candidate.result };
|
|
1057
988
|
}
|
|
1058
|
-
function
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (
|
|
1087
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
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 = {
|
|
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 {
|
|
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
|
-
|
|
2718
|
-
|
|
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.
|
|
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 {
|