@bastani/atomic 0.5.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +956 -0
  3. package/assets/settings.schema.json +52 -0
  4. package/package.json +68 -0
  5. package/src/cli.ts +197 -0
  6. package/src/commands/cli/chat/client.ts +18 -0
  7. package/src/commands/cli/chat/index.ts +247 -0
  8. package/src/commands/cli/chat.ts +8 -0
  9. package/src/commands/cli/config.ts +55 -0
  10. package/src/commands/cli/init/index.ts +452 -0
  11. package/src/commands/cli/init/onboarding.ts +45 -0
  12. package/src/commands/cli/init/scm.ts +190 -0
  13. package/src/commands/cli/init.ts +8 -0
  14. package/src/commands/cli/update.ts +46 -0
  15. package/src/commands/cli/workflow.ts +164 -0
  16. package/src/lib/merge.ts +65 -0
  17. package/src/lib/path-root-guard.ts +38 -0
  18. package/src/lib/spawn.ts +467 -0
  19. package/src/scripts/bump-version.ts +94 -0
  20. package/src/scripts/constants-base.ts +14 -0
  21. package/src/scripts/constants.ts +34 -0
  22. package/src/sdk/components/color-utils.ts +20 -0
  23. package/src/sdk/components/connectors.test.ts +661 -0
  24. package/src/sdk/components/connectors.ts +156 -0
  25. package/src/sdk/components/edge.tsx +11 -0
  26. package/src/sdk/components/error-boundary.tsx +38 -0
  27. package/src/sdk/components/graph-theme.ts +36 -0
  28. package/src/sdk/components/header.tsx +60 -0
  29. package/src/sdk/components/layout.test.ts +924 -0
  30. package/src/sdk/components/layout.ts +186 -0
  31. package/src/sdk/components/node-card.tsx +68 -0
  32. package/src/sdk/components/orchestrator-panel-contexts.ts +26 -0
  33. package/src/sdk/components/orchestrator-panel-store.test.ts +561 -0
  34. package/src/sdk/components/orchestrator-panel-store.ts +118 -0
  35. package/src/sdk/components/orchestrator-panel-types.ts +21 -0
  36. package/src/sdk/components/orchestrator-panel.tsx +143 -0
  37. package/src/sdk/components/session-graph-panel.tsx +364 -0
  38. package/src/sdk/components/status-helpers.ts +32 -0
  39. package/src/sdk/components/statusline.tsx +63 -0
  40. package/src/sdk/define-workflow.ts +98 -0
  41. package/src/sdk/errors.ts +39 -0
  42. package/src/sdk/index.ts +38 -0
  43. package/src/sdk/providers/claude.ts +316 -0
  44. package/src/sdk/providers/copilot.ts +43 -0
  45. package/src/sdk/providers/opencode.ts +43 -0
  46. package/src/sdk/runtime/discovery.ts +172 -0
  47. package/src/sdk/runtime/executor.test.ts +415 -0
  48. package/src/sdk/runtime/executor.ts +695 -0
  49. package/src/sdk/runtime/loader.ts +372 -0
  50. package/src/sdk/runtime/panel.tsx +9 -0
  51. package/src/sdk/runtime/theme.ts +76 -0
  52. package/src/sdk/runtime/tmux.ts +542 -0
  53. package/src/sdk/types.ts +114 -0
  54. package/src/sdk/workflows.ts +85 -0
  55. package/src/services/config/atomic-config.ts +124 -0
  56. package/src/services/config/atomic-global-config.ts +361 -0
  57. package/src/services/config/config-path.ts +19 -0
  58. package/src/services/config/definitions.ts +176 -0
  59. package/src/services/config/index.ts +7 -0
  60. package/src/services/config/settings-schema.ts +2 -0
  61. package/src/services/config/settings.ts +149 -0
  62. package/src/services/system/copy.ts +381 -0
  63. package/src/services/system/detect.ts +161 -0
  64. package/src/services/system/download.ts +325 -0
  65. package/src/services/system/file-lock.ts +289 -0
  66. package/src/services/system/skills.ts +67 -0
  67. package/src/theme/colors.ts +25 -0
  68. package/src/version.ts +7 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * tmux session and pane management utilities.
3
+ *
4
+ * Provides low-level tmux operations for the workflow runtime:
5
+ * creating sessions, splitting panes, spawning commands, capturing output,
6
+ * sending keystrokes, and pane state detection.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Core tmux primitives
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** Cached resolved multiplexer binary path. Resolved once on first use. */
14
+ let resolvedMuxBinary: string | null | undefined; // undefined = not yet resolved
15
+
16
+ /**
17
+ * Resolve the terminal multiplexer binary for the current platform.
18
+ *
19
+ * On Windows, tries psmux → pmux → tmux (psmux ships all three as aliases).
20
+ * On Unix/macOS, uses tmux directly.
21
+ *
22
+ * Returns the binary name (not the full path) or null if none is found.
23
+ * The result is cached after the first call.
24
+ */
25
+ export function getMuxBinary(): string | null {
26
+ if (resolvedMuxBinary !== undefined) return resolvedMuxBinary;
27
+
28
+ if (process.platform === "win32") {
29
+ for (const candidate of ["psmux", "pmux", "tmux"]) {
30
+ if (Bun.which(candidate)) {
31
+ resolvedMuxBinary = candidate;
32
+ return resolvedMuxBinary;
33
+ }
34
+ }
35
+ resolvedMuxBinary = null;
36
+ return null;
37
+ }
38
+
39
+ // Unix / macOS
40
+ resolvedMuxBinary = Bun.which("tmux") ? "tmux" : null;
41
+ return resolvedMuxBinary;
42
+ }
43
+
44
+ /**
45
+ * Reset the cached multiplexer binary resolution.
46
+ * Call after installing tmux/psmux to force re-detection.
47
+ */
48
+ export function resetMuxBinaryCache(): void {
49
+ resolvedMuxBinary = undefined;
50
+ }
51
+
52
+ /**
53
+ * Check if tmux is installed and available.
54
+ */
55
+ export function isTmuxInstalled(): boolean {
56
+ return getMuxBinary() !== null;
57
+ }
58
+
59
+ /**
60
+ * Check if we're currently inside a tmux session.
61
+ */
62
+ export function isInsideTmux(): boolean {
63
+ return process.env.TMUX !== undefined || process.env.PSMUX !== undefined;
64
+ }
65
+
66
+ /**
67
+ * Run a tmux command and return a result object.
68
+ * Prefers this over the throwing `tmux()` for cases where callers
69
+ * need to handle failure gracefully.
70
+ */
71
+ export function tmuxRun(args: string[]): { ok: true; stdout: string } | { ok: false; stderr: string } {
72
+ const binary = getMuxBinary();
73
+ if (!binary) {
74
+ return { ok: false, stderr: "No terminal multiplexer (tmux/psmux) found on PATH" };
75
+ }
76
+ const result = Bun.spawnSync({
77
+ cmd: [binary, ...args],
78
+ stdout: "pipe",
79
+ stderr: "pipe",
80
+ });
81
+ if (!result.success) {
82
+ const stderr = new TextDecoder().decode(result.stderr).trim();
83
+ return { ok: false, stderr };
84
+ }
85
+ return { ok: true, stdout: new TextDecoder().decode(result.stdout).trim() };
86
+ }
87
+
88
+ /**
89
+ * Run a tmux command and return stdout. Throws on failure.
90
+ */
91
+ function tmux(args: string[]): string {
92
+ const result = tmuxRun(args);
93
+ if (!result.ok) {
94
+ throw new Error(`tmux ${args[0]} failed: ${result.stderr}`);
95
+ }
96
+ return result.stdout;
97
+ }
98
+
99
+ /**
100
+ * Run a tmux command, ignoring output. Throws on failure.
101
+ */
102
+ function tmuxExec(args: string[]): void {
103
+ const result = tmuxRun(args);
104
+ if (!result.ok) {
105
+ throw new Error(`tmux ${args[0]} failed: ${result.stderr}`);
106
+ }
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Session and pane management
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Create a new tmux session with the given name.
115
+ * The session starts detached with an initial command in the first pane.
116
+ *
117
+ * @param sessionName - Unique session name
118
+ * @param initialCommand - Shell command to run in the initial pane
119
+ * @param windowName - Optional name for the initial window
120
+ * @param cwd - Optional working directory for the initial pane
121
+ * @returns The pane ID of the initial pane (e.g., "%0")
122
+ */
123
+ export function createSession(sessionName: string, initialCommand: string, windowName?: string, cwd?: string): string {
124
+ const args = [
125
+ "new-session",
126
+ "-d",
127
+ "-s", sessionName,
128
+ "-P", "-F", "#{pane_id}",
129
+ ];
130
+ if (windowName) {
131
+ args.push("-n", windowName);
132
+ }
133
+ if (cwd) {
134
+ args.push("-c", cwd);
135
+ }
136
+ args.push(initialCommand);
137
+ const paneId = tmux(args);
138
+ return paneId || tmux(["list-panes", "-t", sessionName, "-F", "#{pane_id}"]).split("\n")[0]!;
139
+ }
140
+
141
+ /**
142
+ * Create a new window in an existing session without switching focus.
143
+ *
144
+ * @param sessionName - Target session name
145
+ * @param windowName - Name for the new window
146
+ * @param command - Shell command to run in the new window
147
+ * @param cwd - Optional working directory for the new window
148
+ * @returns The pane ID of the new window's pane
149
+ */
150
+ export function createWindow(sessionName: string, windowName: string, command: string, cwd?: string): string {
151
+ const args = [
152
+ "new-window",
153
+ "-d",
154
+ "-t", sessionName,
155
+ "-n", windowName,
156
+ "-P", "-F", "#{pane_id}",
157
+ ];
158
+ if (cwd) {
159
+ args.push("-c", cwd);
160
+ }
161
+ args.push(command);
162
+ return tmux(args);
163
+ }
164
+
165
+ /**
166
+ * Create a new pane in an existing session by splitting.
167
+ *
168
+ * @returns The pane ID of the new pane
169
+ */
170
+ export function createPane(sessionName: string, command: string): string {
171
+ return tmux([
172
+ "split-window",
173
+ "-t", sessionName,
174
+ "-P", "-F", "#{pane_id}",
175
+ command,
176
+ ]);
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Keystroke sending
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /**
184
+ * Send literal text to a tmux pane using `-l` flag (no special key interpretation).
185
+ * Uses `--` to prevent text starting with `-` from being parsed as flags.
186
+ */
187
+ export function sendLiteralText(paneId: string, text: string): void {
188
+ // Replace newlines with spaces to avoid premature submission
189
+ const normalized = text.replace(/[\r\n]+/g, " ");
190
+ tmuxExec(["send-keys", "-t", paneId, "-l", "--", normalized]);
191
+ }
192
+
193
+ /**
194
+ * Send a special key (C-m, C-c, C-u, Tab, etc.) to a tmux pane.
195
+ */
196
+ export function sendSpecialKey(paneId: string, key: string): void {
197
+ tmuxExec(["send-keys", "-t", paneId, key]);
198
+ }
199
+
200
+ /**
201
+ * Send literal text and submit with C-m (carriage return).
202
+ * Uses C-m instead of Enter for raw-mode TUI compatibility.
203
+ *
204
+ * @param presses - Number of C-m presses (default: 1)
205
+ * @param delayMs - Delay between presses in ms (default: 100)
206
+ */
207
+ export function sendKeysAndSubmit(
208
+ paneId: string,
209
+ text: string,
210
+ presses = 1,
211
+ delayMs = 100
212
+ ): void {
213
+ sendLiteralText(paneId, text);
214
+
215
+ for (let i = 0; i < presses; i++) {
216
+ if (i > 0 && delayMs > 0) {
217
+ Bun.sleepSync(delayMs);
218
+ }
219
+ sendSpecialKey(paneId, "C-m");
220
+ }
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Pane capture
225
+ // ---------------------------------------------------------------------------
226
+
227
+ /**
228
+ * Capture the visible content of a tmux pane.
229
+ *
230
+ * @param paneId - The pane ID (e.g., "%0")
231
+ * @param start - Start line (negative = from bottom, default: capture visible only)
232
+ */
233
+ export function capturePane(paneId: string, start?: number): string {
234
+ const args = ["capture-pane", "-t", paneId, "-p"];
235
+ if (start !== undefined) {
236
+ args.push("-S", String(start));
237
+ }
238
+ return tmux(args);
239
+ }
240
+
241
+ /**
242
+ * Capture only the visible portion of a pane (no scrollback).
243
+ * Preferred for state detection (ready/busy) to avoid stale prompt lines
244
+ * or old activity indicators in scrollback triggering false positives.
245
+ * Returns empty string on failure instead of throwing.
246
+ */
247
+ export function capturePaneVisible(paneId: string): string {
248
+ const result = tmuxRun(["capture-pane", "-t", paneId, "-p"]);
249
+ if (!result.ok) return "";
250
+ return result.stdout;
251
+ }
252
+
253
+ /**
254
+ * Capture last N lines of scrollback from a pane.
255
+ * Preferred for output collection where you need recent history.
256
+ * Returns empty string on failure instead of throwing.
257
+ */
258
+ export function capturePaneScrollback(paneId: string, lines = 200): string {
259
+ const result = tmuxRun(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]);
260
+ if (!result.ok) return "";
261
+ return result.stdout;
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Session lifecycle
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /**
269
+ * Kill a tmux session.
270
+ */
271
+ export function killSession(sessionName: string): void {
272
+ try {
273
+ tmuxExec(["kill-session", "-t", sessionName]);
274
+ } catch {
275
+ // Session may already be dead
276
+ }
277
+ }
278
+
279
+ /** Kill a specific tmux window within a session. Silences errors if already dead. */
280
+ export function killWindow(sessionName: string, windowName: string): void {
281
+ try {
282
+ tmuxExec(["kill-window", "-t", `${sessionName}:${windowName}`]);
283
+ } catch {
284
+ // Window may already be dead
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Check if a tmux session exists.
290
+ */
291
+ export function sessionExists(sessionName: string): boolean {
292
+ const binary = getMuxBinary();
293
+ if (!binary) return false;
294
+ const result = Bun.spawnSync({
295
+ cmd: [binary, "has-session", "-t", sessionName],
296
+ stdout: "pipe",
297
+ stderr: "pipe",
298
+ });
299
+ return result.success;
300
+ }
301
+
302
+ /**
303
+ * Attach to an existing tmux session (takes over the current terminal).
304
+ */
305
+ export function attachSession(sessionName: string): void {
306
+ const binary = getMuxBinary();
307
+ if (!binary) {
308
+ throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
309
+ }
310
+ const proc = Bun.spawnSync({
311
+ cmd: [binary, "attach-session", "-t", sessionName],
312
+ stdin: "inherit",
313
+ stdout: "inherit",
314
+ stderr: "pipe",
315
+ });
316
+ if (!proc.success) {
317
+ const stderr = new TextDecoder().decode(proc.stderr).trim();
318
+ throw new Error(`Failed to attach to session: ${sessionName}${stderr ? ` (${stderr})` : ""}`);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Switch the current tmux client to a different session.
324
+ * Use this instead of `attachSession` when already inside tmux to avoid
325
+ * creating a nested tmux client.
326
+ */
327
+ export function switchClient(sessionName: string): void {
328
+ tmuxExec(["switch-client", "-t", sessionName]);
329
+ }
330
+
331
+ /**
332
+ * Get the name of the current tmux session (when running inside tmux).
333
+ * Returns null if not inside tmux or if the query fails.
334
+ */
335
+ export function getCurrentSession(): string | null {
336
+ if (!isInsideTmux()) return null;
337
+ const result = tmuxRun(["display-message", "-p", "#{session_name}"]);
338
+ if (!result.ok) return null;
339
+ return result.stdout || null;
340
+ }
341
+
342
+ /**
343
+ * Attach or switch to a tmux session depending on whether we're already
344
+ * inside tmux. Avoids nested tmux clients.
345
+ *
346
+ * - Outside tmux: spawns `attach-session` (blocks until session ends).
347
+ * - Inside tmux: runs `switch-client` (returns immediately).
348
+ */
349
+ export function attachOrSwitch(sessionName: string): void {
350
+ if (isInsideTmux()) {
351
+ switchClient(sessionName);
352
+ } else {
353
+ attachSession(sessionName);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Select (switch to) a window within the current tmux session.
359
+ */
360
+ export function selectWindow(target: string): void {
361
+ tmuxExec(["select-window", "-t", target]);
362
+ }
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Normalization (ported from oh-my-codex's normalizeTmuxCapture)
366
+ // ---------------------------------------------------------------------------
367
+
368
+ /**
369
+ * Collapse all whitespace to single spaces for robust capture comparison.
370
+ * Prevents false negatives from tmux inserting/stripping whitespace.
371
+ */
372
+ export function normalizeTmuxCapture(text: string): string {
373
+ return text.replace(/\r/g, "").replace(/\s+/g, " ").trim();
374
+ }
375
+
376
+ /**
377
+ * Normalize captured text preserving line structure (for display output).
378
+ */
379
+ export function normalizeTmuxLines(text: string): string {
380
+ return text
381
+ .split("\n")
382
+ .map((l) => l.trimEnd())
383
+ .join("\n")
384
+ .trim();
385
+ }
386
+
387
+ /** Split capture into cleaned, non-empty lines. */
388
+ function toPaneLines(captured: string): string[] {
389
+ return captured
390
+ .split("\n")
391
+ .map((l) => l.replace(/\r/g, "").trimEnd())
392
+ .filter((l) => l.trim() !== "");
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // Pane state detection (ported from oh-my-codex's tmux-hook-engine.ts)
397
+ // ---------------------------------------------------------------------------
398
+
399
+ /** Returns true when the pane is still bootstrapping (loading/initializing). */
400
+ function paneIsBootstrapping(lines: string[]): boolean {
401
+ return lines.some(
402
+ (line) =>
403
+ /\b(loading|initializing|starting up)\b/i.test(line) ||
404
+ /\bmodel:\s*loading\b/i.test(line) ||
405
+ /\bconnecting\s+to\b/i.test(line),
406
+ );
407
+ }
408
+
409
+ /**
410
+ * Returns true when the pane shows an agent prompt ready for input.
411
+ * Detects Claude Code (❯), Codex (›), and generic (>) prompts.
412
+ */
413
+ export function paneLooksReady(captured: string): boolean {
414
+ const content = captured.trimEnd();
415
+ if (content === "") return false;
416
+
417
+ const lines = toPaneLines(content);
418
+ if (paneIsBootstrapping(lines)) return false;
419
+
420
+ if (lines.some((line) => /^\s*[›>❯]\s*/u.test(line))) return true;
421
+ if (lines.some((line) => /\bhow can i help(?: you)?\b/i.test(line))) return true;
422
+
423
+ return false;
424
+ }
425
+
426
+ /**
427
+ * Returns true when the agent has an active task in progress.
428
+ * Checks last 40 lines for known busy indicators.
429
+ */
430
+ export function paneHasActiveTask(captured: string): boolean {
431
+ const tail = toPaneLines(captured)
432
+ .map((line) => line.trim())
433
+ .slice(-40);
434
+
435
+ if (tail.some((l) => /\b\d+\s+background terminal running\b/i.test(l))) return true;
436
+ if (tail.some((l) => /esc to interrupt/i.test(l))) return true;
437
+ if (tail.some((l) => /\bbackground terminal running\b/i.test(l))) return true;
438
+ return tail.some((l) =>
439
+ /^[·✻]\s+[A-Za-z][A-Za-z0-9''-]*(?:\s+[A-Za-z][A-Za-z0-9''-]*){0,3}(?:…|\.{3})$/u.test(l),
440
+ );
441
+ }
442
+
443
+ /**
444
+ * Returns true when the pane is idle — showing a prompt and not processing.
445
+ * Uses visible-only capture to avoid stale scrollback matches.
446
+ */
447
+ export function paneIsIdle(paneId: string): boolean {
448
+ const visible = capturePaneVisible(paneId);
449
+ return paneLooksReady(visible) && !paneHasActiveTask(visible);
450
+ }
451
+
452
+ // ---------------------------------------------------------------------------
453
+ // Readiness wait
454
+ // ---------------------------------------------------------------------------
455
+
456
+ /**
457
+ * Wait for the pane to be idle (prompt visible, no active task) with
458
+ * exponential backoff. Returns the time spent waiting (ms).
459
+ */
460
+ export async function waitForPaneReady(paneId: string, timeoutMs: number = 30_000): Promise<number> {
461
+ const startedAt = Date.now();
462
+ let delayMs = 150;
463
+ const maxDelayMs = 8_000;
464
+
465
+ while (Date.now() - startedAt < timeoutMs) {
466
+ if (paneIsIdle(paneId)) return Date.now() - startedAt;
467
+
468
+ const remaining = timeoutMs - (Date.now() - startedAt);
469
+ if (remaining <= 0) break;
470
+ await Bun.sleep(Math.min(delayMs, remaining));
471
+ delayMs = Math.min(maxDelayMs, delayMs * 2);
472
+ }
473
+
474
+ return Date.now() - startedAt;
475
+ }
476
+
477
+ // ---------------------------------------------------------------------------
478
+ // Submit rounds with per-round verification
479
+ // ---------------------------------------------------------------------------
480
+
481
+ /**
482
+ * Attempt to submit by pressing C-m, verifying after each round.
483
+ * Returns true as soon as the trigger text disappears from the visible
484
+ * capture or an active task is detected.
485
+ */
486
+ export async function attemptSubmitRounds(
487
+ paneId: string,
488
+ normalizedPrompt: string,
489
+ rounds: number,
490
+ pressesPerRound: number = 1,
491
+ ): Promise<boolean> {
492
+ const presses = Math.max(1, Math.floor(pressesPerRound));
493
+
494
+ for (let round = 0; round < rounds; round++) {
495
+ await Bun.sleep(100);
496
+
497
+ for (let press = 0; press < presses; press++) {
498
+ sendSpecialKey(paneId, "C-m");
499
+ if (press < presses - 1) await Bun.sleep(200);
500
+ }
501
+
502
+ await Bun.sleep(140);
503
+
504
+ const visible = capturePaneVisible(paneId);
505
+ if (!normalizeTmuxCapture(visible).includes(normalizedPrompt)) return true;
506
+ if (paneHasActiveTask(visible)) return true;
507
+
508
+ await Bun.sleep(140);
509
+ }
510
+
511
+ return false;
512
+ }
513
+
514
+ // ---------------------------------------------------------------------------
515
+ // Output waiting
516
+ // ---------------------------------------------------------------------------
517
+
518
+ /**
519
+ * Wait for a pattern to appear in a tmux pane's output.
520
+ * Polls the pane content at the given interval until the pattern matches
521
+ * or the timeout is reached.
522
+ *
523
+ * @returns The full pane content when the pattern was found
524
+ */
525
+ export async function waitForOutput(
526
+ paneId: string,
527
+ pattern: RegExp,
528
+ options: { timeoutMs?: number; pollIntervalMs?: number } = {}
529
+ ): Promise<string> {
530
+ const { timeoutMs = 30_000, pollIntervalMs = 500 } = options;
531
+ const deadline = Date.now() + timeoutMs;
532
+
533
+ while (Date.now() < deadline) {
534
+ const content = capturePane(paneId);
535
+ if (pattern.test(content)) {
536
+ return content;
537
+ }
538
+ await Bun.sleep(pollIntervalMs);
539
+ }
540
+
541
+ throw new Error(`Timed out waiting for pattern ${pattern} in pane ${paneId}`);
542
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Workflow SDK Types
3
+ *
4
+ * Uses native SDK types directly — no re-definitions.
5
+ */
6
+
7
+ import type { SessionEvent } from "@github/copilot-sdk";
8
+ import type { SessionPromptResponse } from "@opencode-ai/sdk/v2";
9
+ import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
10
+
11
+ /** Supported agent types */
12
+ export type AgentType = "copilot" | "opencode" | "claude";
13
+
14
+ /**
15
+ * A transcript from a completed session.
16
+ * Provides both the file path and rendered text content.
17
+ */
18
+ export interface Transcript {
19
+ /** Absolute path to the transcript file on disk */
20
+ path: string;
21
+ /** The transcript content (assistant text extracted from messages) */
22
+ content: string;
23
+ }
24
+
25
+ /**
26
+ * A saved message from any provider, stored as JSON.
27
+ * Uses native SDK types directly.
28
+ */
29
+ export type SavedMessage =
30
+ | { provider: "copilot"; data: SessionEvent }
31
+ | { provider: "opencode"; data: SessionPromptResponse }
32
+ | { provider: "claude"; data: SessionMessage };
33
+
34
+ /**
35
+ * Save native message objects from the provider SDK.
36
+ *
37
+ * - **Copilot**: `ctx.save(await session.getMessages())`
38
+ * - **OpenCode**: `ctx.save(result.data)` — the full `{ info, parts }` response
39
+ * - **Claude**: `ctx.save(sessionId)` — auto-reads via `getSessionMessages()`
40
+ */
41
+ export interface SaveTranscript {
42
+ /** Save Copilot SessionEvent[] from session.getMessages() */
43
+ (messages: SessionEvent[]): Promise<void>;
44
+ /** Save OpenCode prompt response `{ info, parts }` from session.prompt().data */
45
+ (response: SessionPromptResponse): Promise<void>;
46
+ /** Save Claude messages — pass the session ID to auto-read transcript */
47
+ (claudeSessionId: string): Promise<void>;
48
+ }
49
+
50
+ /**
51
+ * Session context provided to each session's run() callback at execution time.
52
+ */
53
+ export interface SessionContext {
54
+ /** The agent's server URL (Copilot --ui-server / OpenCode built-in server) */
55
+ serverUrl: string;
56
+ /** The original user prompt from the CLI invocation */
57
+ userPrompt: string;
58
+ /** Which agent is running */
59
+ agent: AgentType;
60
+ /**
61
+ * Get a previous session's transcript as rendered text.
62
+ * Returns `{ path, content }` — path for file triggers, content for embedding.
63
+ */
64
+ transcript(sessionName: string): Promise<Transcript>;
65
+ /**
66
+ * Get a previous session's raw native messages.
67
+ * Returns SavedMessage[] exactly as stored by ctx.save().
68
+ */
69
+ getMessages(sessionName: string): Promise<SavedMessage[]>;
70
+ /**
71
+ * Save this session's output for subsequent sessions.
72
+ * Accepts native SDK message objects only.
73
+ */
74
+ save: SaveTranscript;
75
+ /** Path to this session's storage directory on disk */
76
+ sessionDir: string;
77
+ /** tmux pane ID for this session */
78
+ paneId: string;
79
+ /** Session UUID */
80
+ sessionId: string;
81
+ }
82
+
83
+ /**
84
+ * Options for defining a session in a workflow.
85
+ */
86
+ export interface SessionOptions {
87
+ /** Unique name for this session (used for transcript references) */
88
+ name: string;
89
+ /** Human-readable description */
90
+ description?: string;
91
+ /** The session callback. User writes raw provider-specific SDK code here. */
92
+ run: (ctx: SessionContext) => Promise<void>;
93
+ }
94
+
95
+ /**
96
+ * Options for defining a workflow.
97
+ */
98
+ export interface WorkflowOptions {
99
+ /** Unique workflow name */
100
+ name: string;
101
+ /** Human-readable description */
102
+ description?: string;
103
+ }
104
+
105
+ /**
106
+ * A compiled workflow definition — the sealed output of defineWorkflow().compile().
107
+ */
108
+ export interface WorkflowDefinition {
109
+ readonly __brand: "WorkflowDefinition";
110
+ readonly name: string;
111
+ readonly description: string;
112
+ /** Ordered execution steps. Each step is an array of sessions — length 1 is sequential, length > 1 is parallel. */
113
+ readonly steps: ReadonlyArray<ReadonlyArray<SessionOptions>>;
114
+ }