@bastani/atomic 0.5.2 → 0.5.3-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-1gb5qxz9.js +1 -0
- package/dist/chunk-fdk7tact.js +417 -0
- package/dist/chunk-mn870nrv.js +982 -0
- package/dist/sdk/components/color-utils.d.ts +3 -0
- package/dist/sdk/components/connectors.d.ts +14 -0
- package/dist/sdk/components/connectors.test.d.ts +1 -0
- package/dist/sdk/components/edge.d.ts +3 -0
- package/dist/sdk/components/error-boundary.d.ts +22 -0
- package/dist/sdk/components/graph-theme.d.ts +16 -0
- package/dist/sdk/components/header.d.ts +2 -0
- package/dist/sdk/components/layout.d.ts +26 -0
- package/dist/sdk/components/layout.test.d.ts +1 -0
- package/dist/sdk/components/node-card.d.ts +8 -0
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts +15 -0
- package/dist/sdk/components/orchestrator-panel-store.d.ts +35 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts +16 -0
- package/dist/sdk/components/orchestrator-panel.d.ts +51 -0
- package/dist/sdk/components/session-graph-panel.d.ts +6 -0
- package/dist/sdk/components/status-helpers.d.ts +5 -0
- package/dist/sdk/components/statusline.d.ts +6 -0
- package/dist/sdk/define-workflow.d.ts +77 -0
- package/dist/sdk/define-workflow.test.d.ts +1 -0
- package/dist/sdk/errors.d.ts +21 -0
- package/dist/sdk/index.d.ts +12 -0
- package/dist/sdk/index.js +52 -0
- package/dist/sdk/providers/claude.d.ts +159 -0
- package/dist/sdk/providers/copilot.d.ts +14 -0
- package/dist/sdk/providers/opencode.d.ts +14 -0
- package/dist/sdk/runtime/discovery.d.ts +31 -0
- package/dist/sdk/runtime/executor-entry.d.ts +10 -0
- package/dist/sdk/runtime/executor.d.ts +61 -0
- package/dist/sdk/runtime/executor.test.d.ts +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts +34 -0
- package/dist/sdk/runtime/loader.d.ts +72 -0
- package/dist/sdk/runtime/panel.d.ts +8 -0
- package/dist/sdk/runtime/theme.d.ts +27 -0
- package/dist/sdk/runtime/tmux.d.ts +191 -0
- package/dist/sdk/types.d.ts +214 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts +13 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.js +95 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts +13 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.js +118 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts +16 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +118 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts +19 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts +13 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.js +147 -0
- package/dist/sdk/workflows/index.d.ts +24 -0
- package/dist/sdk/workflows/index.js +94 -0
- package/package.json +33 -6
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/sdk/define-workflow.ts
|
|
3
|
+
class WorkflowBuilder {
|
|
4
|
+
__brand = "WorkflowBuilder";
|
|
5
|
+
options;
|
|
6
|
+
runFn = null;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
run(fn) {
|
|
11
|
+
if (this.runFn) {
|
|
12
|
+
throw new Error("run() can only be called once per workflow.");
|
|
13
|
+
}
|
|
14
|
+
if (typeof fn !== "function") {
|
|
15
|
+
throw new Error(`run() requires a function, got ${typeof fn}.`);
|
|
16
|
+
}
|
|
17
|
+
this.runFn = fn;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
compile() {
|
|
21
|
+
if (!this.runFn) {
|
|
22
|
+
throw new Error(`Workflow "${this.options.name}" has no run callback. ` + `Add a .run(async (ctx) => { ... }) call before .compile().`);
|
|
23
|
+
}
|
|
24
|
+
const runFn = this.runFn;
|
|
25
|
+
return {
|
|
26
|
+
__brand: "WorkflowDefinition",
|
|
27
|
+
name: this.options.name,
|
|
28
|
+
description: this.options.description ?? "",
|
|
29
|
+
run: runFn
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function defineWorkflow(options) {
|
|
34
|
+
if (!options.name || options.name.trim() === "") {
|
|
35
|
+
throw new Error("Workflow name is required.");
|
|
36
|
+
}
|
|
37
|
+
return new WorkflowBuilder(options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/sdk/runtime/discovery.ts
|
|
41
|
+
import { join } from "path";
|
|
42
|
+
import { readdir, writeFile } from "fs/promises";
|
|
43
|
+
import { existsSync, readdirSync } from "fs";
|
|
44
|
+
import { homedir } from "os";
|
|
45
|
+
import ignore from "ignore";
|
|
46
|
+
function getLocalWorkflowsDir(projectRoot) {
|
|
47
|
+
return join(projectRoot, ".atomic", "workflows");
|
|
48
|
+
}
|
|
49
|
+
function getGlobalWorkflowsDir() {
|
|
50
|
+
return join(homedir(), ".atomic", "workflows");
|
|
51
|
+
}
|
|
52
|
+
var AGENTS = ["copilot", "opencode", "claude"];
|
|
53
|
+
var AGENT_SET = new Set(AGENTS);
|
|
54
|
+
var WORKFLOWS_GITIGNORE = [
|
|
55
|
+
"node_modules/",
|
|
56
|
+
"dist/",
|
|
57
|
+
"build/",
|
|
58
|
+
"coverage/",
|
|
59
|
+
".cache/",
|
|
60
|
+
"*.log",
|
|
61
|
+
"*.tsbuildinfo",
|
|
62
|
+
""
|
|
63
|
+
].join(`
|
|
64
|
+
`);
|
|
65
|
+
async function loadWorkflowsGitignore(workflowsDir) {
|
|
66
|
+
const gitignorePath = join(workflowsDir, ".gitignore");
|
|
67
|
+
let content;
|
|
68
|
+
try {
|
|
69
|
+
content = await Bun.file(gitignorePath).text();
|
|
70
|
+
} catch {
|
|
71
|
+
await writeFile(gitignorePath, WORKFLOWS_GITIGNORE);
|
|
72
|
+
content = WORKFLOWS_GITIGNORE;
|
|
73
|
+
}
|
|
74
|
+
return ignore().add(content);
|
|
75
|
+
}
|
|
76
|
+
async function discoverFromBaseDir(baseDir, source, agentFilter) {
|
|
77
|
+
const workflows = [];
|
|
78
|
+
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
79
|
+
const agentNames = new Set(agents);
|
|
80
|
+
let workflowEntries;
|
|
81
|
+
try {
|
|
82
|
+
workflowEntries = await readdir(baseDir, { withFileTypes: true });
|
|
83
|
+
} catch {
|
|
84
|
+
return workflows;
|
|
85
|
+
}
|
|
86
|
+
const ig = await loadWorkflowsGitignore(baseDir);
|
|
87
|
+
for (const wfEntry of workflowEntries) {
|
|
88
|
+
if (!wfEntry.isDirectory())
|
|
89
|
+
continue;
|
|
90
|
+
if (wfEntry.name.startsWith("."))
|
|
91
|
+
continue;
|
|
92
|
+
if (AGENT_SET.has(wfEntry.name))
|
|
93
|
+
continue;
|
|
94
|
+
if (ig.ignores(wfEntry.name + "/"))
|
|
95
|
+
continue;
|
|
96
|
+
const workflowDir = join(baseDir, wfEntry.name);
|
|
97
|
+
let agentEntries;
|
|
98
|
+
try {
|
|
99
|
+
agentEntries = await readdir(workflowDir, { withFileTypes: true });
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const agentEntry of agentEntries) {
|
|
104
|
+
if (!agentEntry.isDirectory())
|
|
105
|
+
continue;
|
|
106
|
+
if (!agentNames.has(agentEntry.name))
|
|
107
|
+
continue;
|
|
108
|
+
const indexPath = join(workflowDir, agentEntry.name, "index.ts");
|
|
109
|
+
const file = Bun.file(indexPath);
|
|
110
|
+
if (await file.exists()) {
|
|
111
|
+
workflows.push({
|
|
112
|
+
name: wfEntry.name,
|
|
113
|
+
agent: agentEntry.name,
|
|
114
|
+
path: indexPath,
|
|
115
|
+
source
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return workflows;
|
|
121
|
+
}
|
|
122
|
+
var BUILTIN_WORKFLOWS_DIR = join(Bun.fileURLToPath(new URL("../workflows/builtin", import.meta.url)));
|
|
123
|
+
function discoverBuiltinWorkflows(agentFilter) {
|
|
124
|
+
const results = [];
|
|
125
|
+
const agents = agentFilter ? [agentFilter] : AGENTS;
|
|
126
|
+
let workflowNames;
|
|
127
|
+
try {
|
|
128
|
+
workflowNames = readdirSync(BUILTIN_WORKFLOWS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
129
|
+
} catch {
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
for (const name of workflowNames) {
|
|
133
|
+
for (const agent of agents) {
|
|
134
|
+
const indexPath = join(BUILTIN_WORKFLOWS_DIR, name, agent, "index.ts");
|
|
135
|
+
if (existsSync(indexPath)) {
|
|
136
|
+
results.push({ name, agent, path: indexPath, source: "builtin" });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
async function discoverWorkflows(projectRoot = process.cwd(), agentFilter) {
|
|
143
|
+
const localDir = getLocalWorkflowsDir(projectRoot);
|
|
144
|
+
const globalDir = getGlobalWorkflowsDir();
|
|
145
|
+
const builtinResults = discoverBuiltinWorkflows(agentFilter);
|
|
146
|
+
const [globalResults, localResults] = await Promise.all([
|
|
147
|
+
discoverFromBaseDir(globalDir, "global", agentFilter),
|
|
148
|
+
discoverFromBaseDir(localDir, "local", agentFilter)
|
|
149
|
+
]);
|
|
150
|
+
const byKey = new Map;
|
|
151
|
+
for (const wf of builtinResults) {
|
|
152
|
+
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
153
|
+
}
|
|
154
|
+
for (const wf of globalResults) {
|
|
155
|
+
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
156
|
+
}
|
|
157
|
+
for (const wf of localResults) {
|
|
158
|
+
byKey.set(`${wf.agent}/${wf.name}`, wf);
|
|
159
|
+
}
|
|
160
|
+
return Array.from(byKey.values());
|
|
161
|
+
}
|
|
162
|
+
async function findWorkflow(name, agent, projectRoot = process.cwd()) {
|
|
163
|
+
const all = await discoverWorkflows(projectRoot, agent);
|
|
164
|
+
return all.find((w) => w.name === name) ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/sdk/providers/copilot.ts
|
|
168
|
+
function validateCopilotWorkflow(source) {
|
|
169
|
+
const warnings = [];
|
|
170
|
+
if (/\bnew\s+CopilotClient\b/.test(source)) {
|
|
171
|
+
warnings.push({
|
|
172
|
+
rule: "copilot/manual-client",
|
|
173
|
+
message: "Manual CopilotClient creation detected. Use s.client instead \u2014 " + "the runtime auto-creates and cleans up the client."
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (/\bclient\.createSession\b/.test(source)) {
|
|
177
|
+
warnings.push({
|
|
178
|
+
rule: "copilot/manual-session",
|
|
179
|
+
message: "Manual createSession() call detected. Use s.session instead \u2014 " + "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage()."
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return warnings;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/sdk/providers/opencode.ts
|
|
186
|
+
function validateOpenCodeWorkflow(source) {
|
|
187
|
+
const warnings = [];
|
|
188
|
+
if (/\bcreateOpencodeClient\b/.test(source)) {
|
|
189
|
+
warnings.push({
|
|
190
|
+
rule: "opencode/manual-client",
|
|
191
|
+
message: "Manual createOpencodeClient() call detected. Use s.client instead \u2014 " + "the runtime auto-creates the client. Pass client config as the second arg to ctx.stage()."
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (/\bclient\.session\.create\b/.test(source)) {
|
|
195
|
+
warnings.push({
|
|
196
|
+
rule: "opencode/manual-session",
|
|
197
|
+
message: "Manual client.session.create() call detected. Use s.session instead \u2014 " + "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage()."
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return warnings;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/sdk/runtime/tmux.ts
|
|
204
|
+
var resolvedMuxBinary;
|
|
205
|
+
function getMuxBinary() {
|
|
206
|
+
if (resolvedMuxBinary !== undefined)
|
|
207
|
+
return resolvedMuxBinary;
|
|
208
|
+
if (process.platform === "win32") {
|
|
209
|
+
for (const candidate of ["psmux", "pmux", "tmux"]) {
|
|
210
|
+
if (Bun.which(candidate)) {
|
|
211
|
+
resolvedMuxBinary = candidate;
|
|
212
|
+
return resolvedMuxBinary;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
resolvedMuxBinary = null;
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
resolvedMuxBinary = Bun.which("tmux") ? "tmux" : null;
|
|
219
|
+
return resolvedMuxBinary;
|
|
220
|
+
}
|
|
221
|
+
function resetMuxBinaryCache() {
|
|
222
|
+
resolvedMuxBinary = undefined;
|
|
223
|
+
}
|
|
224
|
+
function isTmuxInstalled() {
|
|
225
|
+
return getMuxBinary() !== null;
|
|
226
|
+
}
|
|
227
|
+
function isInsideTmux() {
|
|
228
|
+
return process.env.TMUX !== undefined || process.env.PSMUX !== undefined;
|
|
229
|
+
}
|
|
230
|
+
function tmuxRun(args) {
|
|
231
|
+
const binary = getMuxBinary();
|
|
232
|
+
if (!binary) {
|
|
233
|
+
return { ok: false, stderr: "No terminal multiplexer (tmux/psmux) found on PATH" };
|
|
234
|
+
}
|
|
235
|
+
const result = Bun.spawnSync({
|
|
236
|
+
cmd: [binary, ...args],
|
|
237
|
+
stdout: "pipe",
|
|
238
|
+
stderr: "pipe"
|
|
239
|
+
});
|
|
240
|
+
if (!result.success) {
|
|
241
|
+
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
242
|
+
return { ok: false, stderr };
|
|
243
|
+
}
|
|
244
|
+
return { ok: true, stdout: new TextDecoder().decode(result.stdout).trim() };
|
|
245
|
+
}
|
|
246
|
+
function tmux(args) {
|
|
247
|
+
const result = tmuxRun(args);
|
|
248
|
+
if (!result.ok) {
|
|
249
|
+
throw new Error(`tmux ${args[0]} failed: ${result.stderr}`);
|
|
250
|
+
}
|
|
251
|
+
return result.stdout;
|
|
252
|
+
}
|
|
253
|
+
function tmuxExec(args) {
|
|
254
|
+
const result = tmuxRun(args);
|
|
255
|
+
if (!result.ok) {
|
|
256
|
+
throw new Error(`tmux ${args[0]} failed: ${result.stderr}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function buildEnvArgs(envVars) {
|
|
260
|
+
if (!envVars)
|
|
261
|
+
return [];
|
|
262
|
+
const args = [];
|
|
263
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
264
|
+
args.push("-e", `${key}=${value}`);
|
|
265
|
+
}
|
|
266
|
+
return args;
|
|
267
|
+
}
|
|
268
|
+
function createSession(sessionName, initialCommand, windowName, cwd, envVars) {
|
|
269
|
+
const args = [
|
|
270
|
+
"new-session",
|
|
271
|
+
"-d",
|
|
272
|
+
"-s",
|
|
273
|
+
sessionName,
|
|
274
|
+
"-P",
|
|
275
|
+
"-F",
|
|
276
|
+
"#{pane_id}",
|
|
277
|
+
...buildEnvArgs(envVars)
|
|
278
|
+
];
|
|
279
|
+
if (windowName) {
|
|
280
|
+
args.push("-n", windowName);
|
|
281
|
+
}
|
|
282
|
+
if (cwd) {
|
|
283
|
+
args.push("-c", cwd);
|
|
284
|
+
}
|
|
285
|
+
args.push(initialCommand);
|
|
286
|
+
const paneId = tmux(args);
|
|
287
|
+
return paneId || tmux(["list-panes", "-t", sessionName, "-F", "#{pane_id}"]).split(`
|
|
288
|
+
`)[0];
|
|
289
|
+
}
|
|
290
|
+
function createWindow(sessionName, windowName, command, cwd, envVars) {
|
|
291
|
+
const args = [
|
|
292
|
+
"new-window",
|
|
293
|
+
"-d",
|
|
294
|
+
"-t",
|
|
295
|
+
sessionName,
|
|
296
|
+
"-n",
|
|
297
|
+
windowName,
|
|
298
|
+
"-P",
|
|
299
|
+
"-F",
|
|
300
|
+
"#{pane_id}",
|
|
301
|
+
...buildEnvArgs(envVars)
|
|
302
|
+
];
|
|
303
|
+
if (cwd) {
|
|
304
|
+
args.push("-c", cwd);
|
|
305
|
+
}
|
|
306
|
+
args.push(command);
|
|
307
|
+
return tmux(args);
|
|
308
|
+
}
|
|
309
|
+
function createPane(sessionName, command) {
|
|
310
|
+
return tmux([
|
|
311
|
+
"split-window",
|
|
312
|
+
"-t",
|
|
313
|
+
sessionName,
|
|
314
|
+
"-P",
|
|
315
|
+
"-F",
|
|
316
|
+
"#{pane_id}",
|
|
317
|
+
command
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
function sendLiteralText(paneId, text) {
|
|
321
|
+
const normalized = text.replace(/[\r\n]+/g, " ");
|
|
322
|
+
tmuxExec(["send-keys", "-t", paneId, "-l", "--", normalized]);
|
|
323
|
+
}
|
|
324
|
+
function sendSpecialKey(paneId, key) {
|
|
325
|
+
tmuxExec(["send-keys", "-t", paneId, key]);
|
|
326
|
+
}
|
|
327
|
+
function sendKeysAndSubmit(paneId, text, presses = 1, delayMs = 100) {
|
|
328
|
+
sendLiteralText(paneId, text);
|
|
329
|
+
for (let i = 0;i < presses; i++) {
|
|
330
|
+
if (i > 0 && delayMs > 0) {
|
|
331
|
+
Bun.sleepSync(delayMs);
|
|
332
|
+
}
|
|
333
|
+
sendSpecialKey(paneId, "C-m");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function capturePane(paneId, start) {
|
|
337
|
+
const args = ["capture-pane", "-t", paneId, "-p"];
|
|
338
|
+
if (start !== undefined) {
|
|
339
|
+
args.push("-S", String(start));
|
|
340
|
+
}
|
|
341
|
+
return tmux(args);
|
|
342
|
+
}
|
|
343
|
+
function capturePaneVisible(paneId) {
|
|
344
|
+
const result = tmuxRun(["capture-pane", "-t", paneId, "-p"]);
|
|
345
|
+
if (!result.ok)
|
|
346
|
+
return "";
|
|
347
|
+
return result.stdout;
|
|
348
|
+
}
|
|
349
|
+
function capturePaneScrollback(paneId, lines = 200) {
|
|
350
|
+
const result = tmuxRun(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]);
|
|
351
|
+
if (!result.ok)
|
|
352
|
+
return "";
|
|
353
|
+
return result.stdout;
|
|
354
|
+
}
|
|
355
|
+
function killSession(sessionName) {
|
|
356
|
+
try {
|
|
357
|
+
tmuxExec(["kill-session", "-t", sessionName]);
|
|
358
|
+
} catch {}
|
|
359
|
+
}
|
|
360
|
+
function killWindow(sessionName, windowName) {
|
|
361
|
+
try {
|
|
362
|
+
tmuxExec(["kill-window", "-t", `${sessionName}:${windowName}`]);
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
function sessionExists(sessionName) {
|
|
366
|
+
const binary = getMuxBinary();
|
|
367
|
+
if (!binary)
|
|
368
|
+
return false;
|
|
369
|
+
const result = Bun.spawnSync({
|
|
370
|
+
cmd: [binary, "has-session", "-t", sessionName],
|
|
371
|
+
stdout: "pipe",
|
|
372
|
+
stderr: "pipe"
|
|
373
|
+
});
|
|
374
|
+
return result.success;
|
|
375
|
+
}
|
|
376
|
+
function attachSession(sessionName) {
|
|
377
|
+
const binary = getMuxBinary();
|
|
378
|
+
if (!binary) {
|
|
379
|
+
throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
|
|
380
|
+
}
|
|
381
|
+
const proc = Bun.spawnSync({
|
|
382
|
+
cmd: [binary, "attach-session", "-t", sessionName],
|
|
383
|
+
stdin: "inherit",
|
|
384
|
+
stdout: "inherit",
|
|
385
|
+
stderr: "pipe"
|
|
386
|
+
});
|
|
387
|
+
if (!proc.success) {
|
|
388
|
+
const stderr = new TextDecoder().decode(proc.stderr).trim();
|
|
389
|
+
throw new Error(`Failed to attach to session: ${sessionName}${stderr ? ` (${stderr})` : ""}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function switchClient(sessionName) {
|
|
393
|
+
tmuxExec(["switch-client", "-t", sessionName]);
|
|
394
|
+
}
|
|
395
|
+
function getCurrentSession() {
|
|
396
|
+
if (!isInsideTmux())
|
|
397
|
+
return null;
|
|
398
|
+
const result = tmuxRun(["display-message", "-p", "#{session_name}"]);
|
|
399
|
+
if (!result.ok)
|
|
400
|
+
return null;
|
|
401
|
+
return result.stdout || null;
|
|
402
|
+
}
|
|
403
|
+
function attachOrSwitch(sessionName) {
|
|
404
|
+
if (isInsideTmux()) {
|
|
405
|
+
switchClient(sessionName);
|
|
406
|
+
} else {
|
|
407
|
+
attachSession(sessionName);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function selectWindow(target) {
|
|
411
|
+
tmuxExec(["select-window", "-t", target]);
|
|
412
|
+
}
|
|
413
|
+
function normalizeTmuxCapture(text) {
|
|
414
|
+
return text.replace(/\r/g, "").replace(/\s+/g, " ").trim();
|
|
415
|
+
}
|
|
416
|
+
function normalizeTmuxLines(text) {
|
|
417
|
+
return text.split(`
|
|
418
|
+
`).map((l) => l.trimEnd()).join(`
|
|
419
|
+
`).trim();
|
|
420
|
+
}
|
|
421
|
+
function toPaneLines(captured) {
|
|
422
|
+
return captured.split(`
|
|
423
|
+
`).map((l) => l.replace(/\r/g, "").trimEnd()).filter((l) => l.trim() !== "");
|
|
424
|
+
}
|
|
425
|
+
function paneIsBootstrapping(lines) {
|
|
426
|
+
return lines.some((line) => /\b(loading|initializing|starting up)\b/i.test(line) || /\bmodel:\s*loading\b/i.test(line) || /\bconnecting\s+to\b/i.test(line));
|
|
427
|
+
}
|
|
428
|
+
function paneLooksReady(captured) {
|
|
429
|
+
const content = captured.trimEnd();
|
|
430
|
+
if (content === "")
|
|
431
|
+
return false;
|
|
432
|
+
const lines = toPaneLines(content);
|
|
433
|
+
if (paneIsBootstrapping(lines))
|
|
434
|
+
return false;
|
|
435
|
+
if (lines.some((line) => /^\s*[\u203A>\u276F]\s*/u.test(line)))
|
|
436
|
+
return true;
|
|
437
|
+
if (lines.some((line) => /\bhow can i help(?: you)?\b/i.test(line)))
|
|
438
|
+
return true;
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
function paneHasActiveTask(captured) {
|
|
442
|
+
const tail = toPaneLines(captured).map((line) => line.trim()).slice(-40);
|
|
443
|
+
if (tail.some((l) => /\b\d+\s+background terminal running\b/i.test(l)))
|
|
444
|
+
return true;
|
|
445
|
+
if (tail.some((l) => /esc to interrupt/i.test(l)))
|
|
446
|
+
return true;
|
|
447
|
+
if (tail.some((l) => /\bbackground terminal running\b/i.test(l)))
|
|
448
|
+
return true;
|
|
449
|
+
return tail.some((l) => /^[\u00B7\u273B]\s+[A-Za-z][A-Za-z0-9''-]*(?:\s+[A-Za-z][A-Za-z0-9''-]*){0,3}(?:\u2026|\.{3})$/u.test(l));
|
|
450
|
+
}
|
|
451
|
+
function paneIsIdle(paneId) {
|
|
452
|
+
const visible = capturePaneVisible(paneId);
|
|
453
|
+
return paneLooksReady(visible) && !paneHasActiveTask(visible);
|
|
454
|
+
}
|
|
455
|
+
async function waitForPaneReady(paneId, timeoutMs = 30000) {
|
|
456
|
+
const startedAt = Date.now();
|
|
457
|
+
let delayMs = 150;
|
|
458
|
+
const maxDelayMs = 8000;
|
|
459
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
460
|
+
if (paneIsIdle(paneId))
|
|
461
|
+
return Date.now() - startedAt;
|
|
462
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
463
|
+
if (remaining <= 0)
|
|
464
|
+
break;
|
|
465
|
+
await Bun.sleep(Math.min(delayMs, remaining));
|
|
466
|
+
delayMs = Math.min(maxDelayMs, delayMs * 2);
|
|
467
|
+
}
|
|
468
|
+
return Date.now() - startedAt;
|
|
469
|
+
}
|
|
470
|
+
async function attemptSubmitRounds(paneId, normalizedPrompt, rounds, pressesPerRound = 1) {
|
|
471
|
+
const presses = Math.max(1, Math.floor(pressesPerRound));
|
|
472
|
+
for (let round = 0;round < rounds; round++) {
|
|
473
|
+
await Bun.sleep(100);
|
|
474
|
+
for (let press = 0;press < presses; press++) {
|
|
475
|
+
sendSpecialKey(paneId, "C-m");
|
|
476
|
+
if (press < presses - 1)
|
|
477
|
+
await Bun.sleep(200);
|
|
478
|
+
}
|
|
479
|
+
await Bun.sleep(140);
|
|
480
|
+
const visible = capturePaneVisible(paneId);
|
|
481
|
+
if (!normalizeTmuxCapture(visible).includes(normalizedPrompt))
|
|
482
|
+
return true;
|
|
483
|
+
if (paneHasActiveTask(visible))
|
|
484
|
+
return true;
|
|
485
|
+
await Bun.sleep(140);
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
async function waitForOutput(paneId, pattern, options = {}) {
|
|
490
|
+
const { timeoutMs = 30000, pollIntervalMs = 500 } = options;
|
|
491
|
+
const deadline = Date.now() + timeoutMs;
|
|
492
|
+
while (Date.now() < deadline) {
|
|
493
|
+
const content = capturePane(paneId);
|
|
494
|
+
if (pattern.test(content)) {
|
|
495
|
+
return content;
|
|
496
|
+
}
|
|
497
|
+
await Bun.sleep(pollIntervalMs);
|
|
498
|
+
}
|
|
499
|
+
throw new Error(`Timed out waiting for pattern ${pattern} in pane ${paneId}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/sdk/providers/claude.ts
|
|
503
|
+
var initializedPanes = new Set;
|
|
504
|
+
function clearClaudeSession(paneId) {
|
|
505
|
+
initializedPanes.delete(paneId);
|
|
506
|
+
}
|
|
507
|
+
var DEFAULT_CHAT_FLAGS = [
|
|
508
|
+
"--allow-dangerously-skip-permissions",
|
|
509
|
+
"--dangerously-skip-permissions"
|
|
510
|
+
];
|
|
511
|
+
async function createClaudeSession(options) {
|
|
512
|
+
const {
|
|
513
|
+
paneId,
|
|
514
|
+
chatFlags = DEFAULT_CHAT_FLAGS,
|
|
515
|
+
readyTimeoutMs = 30000
|
|
516
|
+
} = options;
|
|
517
|
+
const cmd = ["claude", ...chatFlags].join(" ");
|
|
518
|
+
sendKeysAndSubmit(paneId, cmd);
|
|
519
|
+
await Bun.sleep(1000);
|
|
520
|
+
await waitForPaneReady(paneId, readyTimeoutMs);
|
|
521
|
+
const visible = capturePaneVisible(paneId);
|
|
522
|
+
if (!paneLooksReady(visible) && !paneHasActiveTask(visible)) {
|
|
523
|
+
throw new Error("createClaudeSession() timed out waiting for the Claude TUI to start. " + "Verify the `claude` command is installed and the flags are valid.");
|
|
524
|
+
}
|
|
525
|
+
initializedPanes.add(paneId);
|
|
526
|
+
}
|
|
527
|
+
async function claudeQuery(options) {
|
|
528
|
+
const {
|
|
529
|
+
paneId,
|
|
530
|
+
prompt,
|
|
531
|
+
timeoutMs = 300000,
|
|
532
|
+
pollIntervalMs = 2000,
|
|
533
|
+
submitPresses = 1,
|
|
534
|
+
maxSubmitRounds = 6,
|
|
535
|
+
readyTimeoutMs = 30000
|
|
536
|
+
} = options;
|
|
537
|
+
if (!initializedPanes.has(paneId)) {
|
|
538
|
+
throw new Error("claudeQuery() called without a prior createClaudeSession() for this pane. " + "Call createClaudeSession({ paneId }) first to start the Claude CLI.");
|
|
539
|
+
}
|
|
540
|
+
const normalizedPrompt = normalizeTmuxCapture(prompt).slice(0, 100);
|
|
541
|
+
const waitElapsed = await waitForPaneReady(paneId, readyTimeoutMs);
|
|
542
|
+
const responseTimeoutMs = Math.max(0, timeoutMs - waitElapsed);
|
|
543
|
+
if (waitElapsed > timeoutMs * 0.5) {
|
|
544
|
+
console.warn(`claudeQuery: readiness wait consumed ${Math.round(waitElapsed / 1000)}s ` + `of ${Math.round(timeoutMs / 1000)}s total timeout budget`);
|
|
545
|
+
}
|
|
546
|
+
const beforeContent = normalizeTmuxLines(capturePaneScrollback(paneId));
|
|
547
|
+
sendLiteralText(paneId, prompt);
|
|
548
|
+
await Bun.sleep(150);
|
|
549
|
+
let delivered = await attemptSubmitRounds(paneId, normalizedPrompt, maxSubmitRounds, submitPresses);
|
|
550
|
+
if (!delivered) {
|
|
551
|
+
const visibleCapture = capturePaneVisible(paneId);
|
|
552
|
+
const visibleNorm = normalizeTmuxCapture(visibleCapture);
|
|
553
|
+
if (visibleNorm.includes(normalizedPrompt) && !paneHasActiveTask(visibleCapture) && paneLooksReady(visibleCapture)) {
|
|
554
|
+
sendSpecialKey(paneId, "C-u");
|
|
555
|
+
await Bun.sleep(80);
|
|
556
|
+
sendLiteralText(paneId, prompt);
|
|
557
|
+
await Bun.sleep(120);
|
|
558
|
+
delivered = await attemptSubmitRounds(paneId, normalizedPrompt, 4, submitPresses);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (!delivered) {
|
|
562
|
+
sendSpecialKey(paneId, "C-m");
|
|
563
|
+
await Bun.sleep(120);
|
|
564
|
+
sendSpecialKey(paneId, "C-m");
|
|
565
|
+
await Bun.sleep(300);
|
|
566
|
+
const verifyCapture = capturePaneVisible(paneId);
|
|
567
|
+
if (paneHasActiveTask(verifyCapture)) {
|
|
568
|
+
delivered = true;
|
|
569
|
+
} else {
|
|
570
|
+
delivered = !normalizeTmuxCapture(verifyCapture).includes(normalizedPrompt);
|
|
571
|
+
}
|
|
572
|
+
if (!delivered) {
|
|
573
|
+
sendSpecialKey(paneId, "C-m");
|
|
574
|
+
await Bun.sleep(150);
|
|
575
|
+
sendSpecialKey(paneId, "C-m");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const deadline = Date.now() + responseTimeoutMs;
|
|
579
|
+
let lastContent = "";
|
|
580
|
+
let stableCount = 0;
|
|
581
|
+
await Bun.sleep(3000);
|
|
582
|
+
while (Date.now() < deadline) {
|
|
583
|
+
const currentContent = normalizeTmuxLines(capturePaneScrollback(paneId));
|
|
584
|
+
if (currentContent === beforeContent) {
|
|
585
|
+
await Bun.sleep(pollIntervalMs);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const visible = capturePaneVisible(paneId);
|
|
589
|
+
if (paneLooksReady(visible) && !paneHasActiveTask(visible)) {
|
|
590
|
+
return { output: currentContent, delivered };
|
|
591
|
+
}
|
|
592
|
+
if (currentContent === lastContent) {
|
|
593
|
+
stableCount++;
|
|
594
|
+
if (stableCount >= 3) {
|
|
595
|
+
return { output: currentContent, delivered };
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
stableCount = 0;
|
|
599
|
+
}
|
|
600
|
+
lastContent = currentContent;
|
|
601
|
+
await Bun.sleep(pollIntervalMs);
|
|
602
|
+
}
|
|
603
|
+
return { output: lastContent || capturePaneScrollback(paneId), delivered };
|
|
604
|
+
}
|
|
605
|
+
function validateClaudeWorkflow(source) {
|
|
606
|
+
const warnings = [];
|
|
607
|
+
if (/\bcreateClaudeSession\b/.test(source)) {
|
|
608
|
+
warnings.push({
|
|
609
|
+
rule: "claude/manual-session",
|
|
610
|
+
message: "Manual createClaudeSession() call detected. The runtime auto-starts the Claude CLI \u2014 " + "use s.session.query() instead of claudeQuery(). Pass chatFlags via the second arg to ctx.stage()."
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
if (/\bclaudeQuery\b/.test(source)) {
|
|
614
|
+
warnings.push({
|
|
615
|
+
rule: "claude/manual-query",
|
|
616
|
+
message: "Direct claudeQuery() call detected. Use s.session.query(prompt) instead \u2014 " + "it wraps claudeQuery with the correct paneId."
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return warnings;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/sdk/runtime/loader.ts
|
|
623
|
+
var WorkflowLoader;
|
|
624
|
+
((WorkflowLoader) => {
|
|
625
|
+
async function resolve(plan) {
|
|
626
|
+
try {
|
|
627
|
+
const file = Bun.file(plan.path);
|
|
628
|
+
if (!await file.exists()) {
|
|
629
|
+
return {
|
|
630
|
+
ok: false,
|
|
631
|
+
stage: "resolve",
|
|
632
|
+
error: new Error(`Workflow file not found: ${plan.path}`),
|
|
633
|
+
message: `Workflow file not found: ${plan.path}`
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
return { ok: true, value: plan };
|
|
637
|
+
} catch (error) {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
stage: "resolve",
|
|
641
|
+
error,
|
|
642
|
+
message: error instanceof Error ? error.message : String(error)
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
WorkflowLoader.resolve = resolve;
|
|
647
|
+
function validateSource(source, agent) {
|
|
648
|
+
switch (agent) {
|
|
649
|
+
case "copilot":
|
|
650
|
+
return validateCopilotWorkflow(source);
|
|
651
|
+
case "opencode":
|
|
652
|
+
return validateOpenCodeWorkflow(source);
|
|
653
|
+
case "claude":
|
|
654
|
+
return validateClaudeWorkflow(source);
|
|
655
|
+
default:
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async function validate(resolved) {
|
|
660
|
+
try {
|
|
661
|
+
const source = await Bun.file(resolved.path).text();
|
|
662
|
+
const warnings = validateSource(source, resolved.agent);
|
|
663
|
+
return {
|
|
664
|
+
ok: true,
|
|
665
|
+
value: { ...resolved, warnings }
|
|
666
|
+
};
|
|
667
|
+
} catch (error) {
|
|
668
|
+
return {
|
|
669
|
+
ok: false,
|
|
670
|
+
stage: "validate",
|
|
671
|
+
error,
|
|
672
|
+
message: error instanceof Error ? error.message : String(error)
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
WorkflowLoader.validate = validate;
|
|
677
|
+
async function load(validated) {
|
|
678
|
+
try {
|
|
679
|
+
const mod = await import(validated.path);
|
|
680
|
+
const definition = mod.default ?? mod;
|
|
681
|
+
if (!definition || definition.__brand !== "WorkflowDefinition") {
|
|
682
|
+
if (definition && definition.__brand === "WorkflowBuilder") {
|
|
683
|
+
return {
|
|
684
|
+
ok: false,
|
|
685
|
+
stage: "load",
|
|
686
|
+
error: new Error("Workflow not compiled"),
|
|
687
|
+
message: `Workflow at ${validated.path} was defined but not compiled.
|
|
688
|
+
Add .compile() at the end of your defineWorkflow() chain:
|
|
689
|
+
|
|
690
|
+
export default defineWorkflow({ ... })
|
|
691
|
+
.run(async (ctx) => { ... })
|
|
692
|
+
.compile();`
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
ok: false,
|
|
697
|
+
stage: "load",
|
|
698
|
+
error: new Error("Invalid workflow export"),
|
|
699
|
+
message: `${validated.path} does not export a valid WorkflowDefinition.
|
|
700
|
+
Make sure it exports defineWorkflow(...).run(...).compile() as the default export.`
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
ok: true,
|
|
705
|
+
value: { ...validated, definition }
|
|
706
|
+
};
|
|
707
|
+
} catch (error) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
stage: "load",
|
|
711
|
+
error,
|
|
712
|
+
message: error instanceof Error ? error.message : String(error)
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
WorkflowLoader.load = load;
|
|
717
|
+
async function loadWorkflow(plan, report) {
|
|
718
|
+
report?.start?.("resolve");
|
|
719
|
+
const resolved = await resolve(plan);
|
|
720
|
+
if (!resolved.ok) {
|
|
721
|
+
report?.error?.("resolve", resolved.error, resolved.message);
|
|
722
|
+
return resolved;
|
|
723
|
+
}
|
|
724
|
+
report?.start?.("validate");
|
|
725
|
+
const validated = await validate(resolved.value);
|
|
726
|
+
if (!validated.ok) {
|
|
727
|
+
report?.error?.("validate", validated.error, validated.message);
|
|
728
|
+
return validated;
|
|
729
|
+
}
|
|
730
|
+
if (validated.value.warnings.length > 0) {
|
|
731
|
+
report?.warn?.(validated.value.warnings);
|
|
732
|
+
}
|
|
733
|
+
report?.start?.("load");
|
|
734
|
+
const loaded = await load(validated.value);
|
|
735
|
+
if (!loaded.ok) {
|
|
736
|
+
report?.error?.("load", loaded.error, loaded.message);
|
|
737
|
+
return loaded;
|
|
738
|
+
}
|
|
739
|
+
return loaded;
|
|
740
|
+
}
|
|
741
|
+
WorkflowLoader.loadWorkflow = loadWorkflow;
|
|
742
|
+
})(WorkflowLoader ||= {});
|
|
743
|
+
|
|
744
|
+
// src/sdk/runtime/executor.ts
|
|
745
|
+
import { join as join2, resolve } from "path";
|
|
746
|
+
import { homedir as homedir2 } from "os";
|
|
747
|
+
import { mkdir, writeFile as writeFile2, readFile } from "fs/promises";
|
|
748
|
+
|
|
749
|
+
// src/sdk/components/orchestrator-panel.tsx
|
|
750
|
+
import { createCliRenderer } from "@opentui/core";
|
|
751
|
+
import { createRoot } from "@opentui/react";
|
|
752
|
+
|
|
753
|
+
// src/sdk/components/orchestrator-panel-store.ts
|
|
754
|
+
class PanelStore {
|
|
755
|
+
version = 0;
|
|
756
|
+
workflowName = "";
|
|
757
|
+
agent = "";
|
|
758
|
+
prompt = "";
|
|
759
|
+
sessions = [];
|
|
760
|
+
completionInfo = null;
|
|
761
|
+
fatalError = null;
|
|
762
|
+
completionReached = false;
|
|
763
|
+
exitResolve = null;
|
|
764
|
+
abortResolve = null;
|
|
765
|
+
listeners = new Set;
|
|
766
|
+
subscribe = (fn) => {
|
|
767
|
+
this.listeners.add(fn);
|
|
768
|
+
return () => this.listeners.delete(fn);
|
|
769
|
+
};
|
|
770
|
+
emit() {
|
|
771
|
+
this.version++;
|
|
772
|
+
for (const fn of this.listeners)
|
|
773
|
+
fn();
|
|
774
|
+
}
|
|
775
|
+
setWorkflowInfo(name, agent, sessions, prompt) {
|
|
776
|
+
this.workflowName = name;
|
|
777
|
+
this.agent = agent;
|
|
778
|
+
this.prompt = prompt;
|
|
779
|
+
this.sessions = [
|
|
780
|
+
{
|
|
781
|
+
name: "orchestrator",
|
|
782
|
+
status: "running",
|
|
783
|
+
parents: [],
|
|
784
|
+
startedAt: Date.now(),
|
|
785
|
+
endedAt: null
|
|
786
|
+
},
|
|
787
|
+
...sessions.map((s) => ({
|
|
788
|
+
name: s.name,
|
|
789
|
+
status: "pending",
|
|
790
|
+
parents: s.parents.length > 0 ? s.parents : ["orchestrator"],
|
|
791
|
+
startedAt: null,
|
|
792
|
+
endedAt: null
|
|
793
|
+
}))
|
|
794
|
+
];
|
|
795
|
+
this.emit();
|
|
796
|
+
}
|
|
797
|
+
startSession(name) {
|
|
798
|
+
const session = this.sessions.find((s) => s.name === name);
|
|
799
|
+
if (!session)
|
|
800
|
+
return;
|
|
801
|
+
session.status = "running";
|
|
802
|
+
session.startedAt = Date.now();
|
|
803
|
+
this.emit();
|
|
804
|
+
}
|
|
805
|
+
completeSession(name) {
|
|
806
|
+
const session = this.sessions.find((s) => s.name === name);
|
|
807
|
+
if (!session)
|
|
808
|
+
return;
|
|
809
|
+
session.status = "complete";
|
|
810
|
+
session.endedAt = Date.now();
|
|
811
|
+
this.emit();
|
|
812
|
+
}
|
|
813
|
+
failSession(name, error) {
|
|
814
|
+
const session = this.sessions.find((s) => s.name === name);
|
|
815
|
+
if (!session)
|
|
816
|
+
return;
|
|
817
|
+
session.status = "error";
|
|
818
|
+
session.error = error;
|
|
819
|
+
session.endedAt = Date.now();
|
|
820
|
+
this.emit();
|
|
821
|
+
}
|
|
822
|
+
addSession(session) {
|
|
823
|
+
this.sessions.push(session);
|
|
824
|
+
this.emit();
|
|
825
|
+
}
|
|
826
|
+
setCompletion(workflowName, transcriptsPath) {
|
|
827
|
+
this.completionInfo = { workflowName, transcriptsPath };
|
|
828
|
+
const orch = this.sessions.find((s) => s.name === "orchestrator");
|
|
829
|
+
if (orch) {
|
|
830
|
+
orch.status = "complete";
|
|
831
|
+
orch.endedAt = Date.now();
|
|
832
|
+
}
|
|
833
|
+
this.emit();
|
|
834
|
+
}
|
|
835
|
+
setFatalError(message) {
|
|
836
|
+
this.fatalError = message;
|
|
837
|
+
this.completionReached = true;
|
|
838
|
+
const orch = this.sessions.find((s) => s.name === "orchestrator");
|
|
839
|
+
if (orch) {
|
|
840
|
+
orch.status = "error";
|
|
841
|
+
orch.endedAt = Date.now();
|
|
842
|
+
}
|
|
843
|
+
this.emit();
|
|
844
|
+
}
|
|
845
|
+
resolveExit() {
|
|
846
|
+
if (this.exitResolve) {
|
|
847
|
+
const resolve = this.exitResolve;
|
|
848
|
+
this.exitResolve = null;
|
|
849
|
+
resolve();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
resolveAbort() {
|
|
853
|
+
if (this.abortResolve) {
|
|
854
|
+
const resolve = this.abortResolve;
|
|
855
|
+
this.abortResolve = null;
|
|
856
|
+
resolve();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
requestQuit() {
|
|
860
|
+
if (this.completionReached) {
|
|
861
|
+
this.resolveExit();
|
|
862
|
+
} else {
|
|
863
|
+
this.resolveAbort();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
markCompletionReached() {
|
|
867
|
+
this.completionReached = true;
|
|
868
|
+
this.emit();
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/sdk/components/orchestrator-panel-contexts.ts
|
|
873
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
874
|
+
var StoreContext = createContext(null);
|
|
875
|
+
var ThemeContext = createContext(null);
|
|
876
|
+
var TmuxSessionContext = createContext("");
|
|
877
|
+
|
|
878
|
+
// src/sdk/components/session-graph-panel.tsx
|
|
879
|
+
import {
|
|
880
|
+
useKeyboard,
|
|
881
|
+
useTerminalDimensions,
|
|
882
|
+
useRenderer
|
|
883
|
+
} from "@opentui/react";
|
|
884
|
+
import {
|
|
885
|
+
useState,
|
|
886
|
+
useEffect,
|
|
887
|
+
useMemo,
|
|
888
|
+
useCallback,
|
|
889
|
+
useRef,
|
|
890
|
+
useContext as useContext2
|
|
891
|
+
} from "react";
|
|
892
|
+
|
|
893
|
+
// src/sdk/components/node-card.tsx
|
|
894
|
+
import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
|
|
895
|
+
|
|
896
|
+
// src/sdk/components/edge.tsx
|
|
897
|
+
import { jsxDEV as jsxDEV2 } from "@opentui/react/jsx-dev-runtime";
|
|
898
|
+
|
|
899
|
+
// src/sdk/components/header.tsx
|
|
900
|
+
import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
|
|
901
|
+
|
|
902
|
+
// src/sdk/components/statusline.tsx
|
|
903
|
+
import { jsxDEV as jsxDEV4 } from "@opentui/react/jsx-dev-runtime";
|
|
904
|
+
|
|
905
|
+
// src/sdk/components/session-graph-panel.tsx
|
|
906
|
+
import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
|
|
907
|
+
|
|
908
|
+
// src/sdk/components/error-boundary.tsx
|
|
909
|
+
import { Component } from "react";
|
|
910
|
+
|
|
911
|
+
// src/sdk/components/orchestrator-panel.tsx
|
|
912
|
+
import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
|
|
913
|
+
// src/sdk/runtime/executor.ts
|
|
914
|
+
function generateId() {
|
|
915
|
+
return crypto.randomUUID().slice(0, 8);
|
|
916
|
+
}
|
|
917
|
+
function getSessionsBaseDir() {
|
|
918
|
+
return join2(homedir2(), ".atomic", "sessions");
|
|
919
|
+
}
|
|
920
|
+
async function ensureDir(dir) {
|
|
921
|
+
await mkdir(dir, { recursive: true });
|
|
922
|
+
}
|
|
923
|
+
function escBash(s) {
|
|
924
|
+
return s.replace(/\x00/g, "").replace(/[\n\r]+/g, " ").replace(/[\\"$`!]/g, "\\$&");
|
|
925
|
+
}
|
|
926
|
+
function escPwsh(s) {
|
|
927
|
+
return s.replace(/\x00/g, "").replace(/[`"$]/g, "`$&").replace(/\n/g, "`n").replace(/\r/g, "`r");
|
|
928
|
+
}
|
|
929
|
+
async function executeWorkflow(options) {
|
|
930
|
+
const {
|
|
931
|
+
definition,
|
|
932
|
+
agent,
|
|
933
|
+
prompt,
|
|
934
|
+
workflowFile,
|
|
935
|
+
projectRoot = process.cwd()
|
|
936
|
+
} = options;
|
|
937
|
+
const workflowRunId = generateId();
|
|
938
|
+
const tmuxSessionName = `atomic-wf-${definition.name}-${workflowRunId}`;
|
|
939
|
+
const sessionsBaseDir = join2(getSessionsBaseDir(), workflowRunId);
|
|
940
|
+
await ensureDir(sessionsBaseDir);
|
|
941
|
+
const thisFile = resolve(import.meta.dir, "executor-entry.ts");
|
|
942
|
+
const isWin = process.platform === "win32";
|
|
943
|
+
const launcherExt = isWin ? "ps1" : "sh";
|
|
944
|
+
const launcherPath = join2(sessionsBaseDir, `orchestrator.${launcherExt}`);
|
|
945
|
+
const logPath = join2(sessionsBaseDir, "orchestrator.log");
|
|
946
|
+
const launcherScript = isWin ? [
|
|
947
|
+
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
948
|
+
`$env:ATOMIC_WF_ID = "${escPwsh(workflowRunId)}"`,
|
|
949
|
+
`$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
|
|
950
|
+
`$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
|
|
951
|
+
`$env:ATOMIC_WF_PROMPT = "${escPwsh(Buffer.from(prompt).toString("base64"))}"`,
|
|
952
|
+
`$env:ATOMIC_WF_FILE = "${escPwsh(workflowFile)}"`,
|
|
953
|
+
`$env:ATOMIC_WF_CWD = "${escPwsh(projectRoot)}"`,
|
|
954
|
+
`bun run "${escPwsh(thisFile)}" 2>"${escPwsh(logPath)}"`
|
|
955
|
+
].join(`
|
|
956
|
+
`) : [
|
|
957
|
+
"#!/bin/bash",
|
|
958
|
+
`cd "${escBash(projectRoot)}"`,
|
|
959
|
+
`export ATOMIC_WF_ID="${escBash(workflowRunId)}"`,
|
|
960
|
+
`export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
|
|
961
|
+
`export ATOMIC_WF_AGENT="${escBash(agent)}"`,
|
|
962
|
+
`export ATOMIC_WF_PROMPT="${escBash(Buffer.from(prompt).toString("base64"))}"`,
|
|
963
|
+
`export ATOMIC_WF_FILE="${escBash(workflowFile)}"`,
|
|
964
|
+
`export ATOMIC_WF_CWD="${escBash(projectRoot)}"`,
|
|
965
|
+
`bun run "${escBash(thisFile)}" 2>"${escBash(logPath)}"`
|
|
966
|
+
].join(`
|
|
967
|
+
`);
|
|
968
|
+
await writeFile2(launcherPath, launcherScript, { mode: 493 });
|
|
969
|
+
const shellCmd = isWin ? `pwsh -NoProfile -File "${escPwsh(launcherPath)}"` : `bash "${escBash(launcherPath)}"`;
|
|
970
|
+
createSession(tmuxSessionName, shellCmd, "orchestrator");
|
|
971
|
+
if (isInsideTmux()) {
|
|
972
|
+
switchClient(tmuxSessionName);
|
|
973
|
+
} else {
|
|
974
|
+
const muxBinary = getMuxBinary() ?? "tmux";
|
|
975
|
+
const attachProc = Bun.spawn([muxBinary, "attach-session", "-t", tmuxSessionName], {
|
|
976
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
977
|
+
});
|
|
978
|
+
await attachProc.exited;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export { WorkflowBuilder, defineWorkflow, AGENTS, WORKFLOWS_GITIGNORE, discoverWorkflows, findWorkflow, validateCopilotWorkflow, validateOpenCodeWorkflow, getMuxBinary, resetMuxBinaryCache, isTmuxInstalled, isInsideTmux, tmuxRun, createSession, createWindow, createPane, sendLiteralText, sendSpecialKey, sendKeysAndSubmit, capturePane, capturePaneVisible, capturePaneScrollback, killSession, killWindow, sessionExists, attachSession, switchClient, getCurrentSession, attachOrSwitch, selectWindow, normalizeTmuxCapture, normalizeTmuxLines, paneLooksReady, paneHasActiveTask, paneIsIdle, waitForPaneReady, attemptSubmitRounds, waitForOutput, clearClaudeSession, createClaudeSession, claudeQuery, validateClaudeWorkflow, WorkflowLoader, executeWorkflow };
|