@bastani/atomic 0.5.3 → 0.5.4-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.
Files changed (48) hide show
  1. package/README.md +110 -11
  2. package/dist/{chunk-mn870nrv.js → chunk-xkxndz5g.js} +213 -154
  3. package/dist/sdk/components/workflow-picker-panel.d.ts +120 -0
  4. package/dist/sdk/define-workflow.d.ts +1 -1
  5. package/dist/sdk/index.js +1 -1
  6. package/dist/sdk/runtime/discovery.d.ts +57 -3
  7. package/dist/sdk/runtime/executor.d.ts +15 -2
  8. package/dist/sdk/runtime/tmux.d.ts +9 -0
  9. package/dist/sdk/types.d.ts +63 -4
  10. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +61 -0
  11. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +48 -0
  12. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +25 -0
  13. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +91 -0
  14. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +56 -0
  15. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +48 -0
  16. package/dist/sdk/workflows/builtin/ralph/claude/index.js +6 -5
  17. package/dist/sdk/workflows/builtin/ralph/copilot/index.js +6 -5
  18. package/dist/sdk/workflows/builtin/ralph/opencode/index.js +6 -5
  19. package/dist/sdk/workflows/index.d.ts +4 -4
  20. package/dist/sdk/workflows/index.js +7 -1
  21. package/package.json +1 -1
  22. package/src/cli.ts +25 -3
  23. package/src/commands/cli/chat/index.ts +5 -5
  24. package/src/commands/cli/init/index.ts +79 -77
  25. package/src/commands/cli/workflow-command.test.ts +757 -0
  26. package/src/commands/cli/workflow.test.ts +310 -0
  27. package/src/commands/cli/workflow.ts +445 -105
  28. package/src/sdk/components/workflow-picker-panel.tsx +1462 -0
  29. package/src/sdk/define-workflow.test.ts +101 -0
  30. package/src/sdk/define-workflow.ts +62 -2
  31. package/src/sdk/runtime/discovery.ts +111 -8
  32. package/src/sdk/runtime/executor.ts +89 -32
  33. package/src/sdk/runtime/tmux.conf +55 -0
  34. package/src/sdk/runtime/tmux.ts +34 -10
  35. package/src/sdk/types.ts +67 -4
  36. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +294 -0
  37. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +276 -0
  38. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +38 -0
  39. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +816 -0
  40. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +334 -0
  41. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +284 -0
  42. package/src/sdk/workflows/builtin/ralph/claude/index.ts +8 -4
  43. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +10 -4
  44. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +8 -4
  45. package/src/sdk/workflows/index.ts +9 -1
  46. package/src/services/system/auto-sync.ts +1 -1
  47. package/src/services/system/install-ui.ts +109 -39
  48. package/src/theme/colors.ts +65 -1
package/README.md CHANGED
@@ -167,11 +167,16 @@ export default defineWorkflow<"claude">({
167
167
  description: "Research -> Implement -> Review",
168
168
  })
169
169
  .run(async (ctx) => {
170
+ // Free-form workflows receive their positional prompt under
171
+ // `ctx.inputs.prompt`. Destructure it once so every stage below
172
+ // can close over a bare string.
173
+ const prompt = ctx.inputs.prompt ?? "";
174
+
170
175
  const research = await ctx.stage(
171
176
  { name: "research", description: "Analyze the codebase" },
172
177
  {}, {},
173
178
  async (s) => {
174
- await s.session.query(`/research-codebase ${s.userPrompt}`);
179
+ await s.session.query(`/research-codebase ${prompt}`);
175
180
  s.save(s.sessionId);
176
181
  },
177
182
  );
@@ -250,11 +255,13 @@ export default defineWorkflow<"claude">({
250
255
  description: "Two-session pipeline: describe -> summarize",
251
256
  })
252
257
  .run(async (ctx) => {
258
+ const prompt = ctx.inputs.prompt ?? "";
259
+
253
260
  const describe = await ctx.stage(
254
261
  { name: "describe", description: "Ask Claude to describe the project" },
255
262
  {}, {},
256
263
  async (s) => {
257
- await s.session.query(s.userPrompt);
264
+ await s.session.query(prompt);
258
265
  s.save(s.sessionId);
259
266
  },
260
267
  );
@@ -287,10 +294,12 @@ export default defineWorkflow<"claude">({
287
294
  description: "describe -> [summarize-a, summarize-b] -> merge",
288
295
  })
289
296
  .run(async (ctx) => {
297
+ const prompt = ctx.inputs.prompt ?? "";
298
+
290
299
  const describe = await ctx.stage(
291
300
  { name: "describe" }, {}, {},
292
301
  async (s) => {
293
- await s.session.query(s.userPrompt);
302
+ await s.session.query(prompt);
294
303
  s.save(s.sessionId);
295
304
  },
296
305
  );
@@ -322,6 +331,71 @@ export default defineWorkflow<"claude">({
322
331
 
323
332
  </details>
324
333
 
334
+ <details>
335
+ <summary>Example: Structured-input workflow (declared schema + CLI flag validation)</summary>
336
+
337
+ Declare an `inputs` array on `defineWorkflow` and the CLI materialises one `--<field>=<value>` flag per entry. Required fields, enum membership, and unknown-flag rejection are all validated before any tmux session is spawned. The interactive picker (`atomic workflow -a <agent>`) renders the same schema as a form.
338
+
339
+ ```ts
340
+ // .atomic/workflows/gen-spec/claude/index.ts
341
+ import { defineWorkflow } from "@bastani/atomic/workflows";
342
+
343
+ export default defineWorkflow<"claude">({
344
+ name: "gen-spec",
345
+ description: "Convert a research doc into an execution spec",
346
+ inputs: [
347
+ {
348
+ name: "research_doc",
349
+ type: "string",
350
+ required: true,
351
+ description: "path to the research doc",
352
+ placeholder: "research/docs/2026-04-11-auth.md",
353
+ },
354
+ {
355
+ name: "focus",
356
+ type: "enum",
357
+ required: true,
358
+ description: "how aggressively to scope the spec",
359
+ values: ["minimal", "standard", "exhaustive"],
360
+ default: "standard",
361
+ },
362
+ {
363
+ name: "notes",
364
+ type: "text",
365
+ description: "extra guidance for the spec writer (optional)",
366
+ },
367
+ ],
368
+ })
369
+ .run(async (ctx) => {
370
+ // Read each declared field by name.
371
+ const { research_doc, focus } = ctx.inputs;
372
+ const notes = ctx.inputs.notes ?? "";
373
+
374
+ await ctx.stage({ name: "write-spec" }, {}, {}, async (s) => {
375
+ await s.session.query(
376
+ `Read ${research_doc} and produce a ${focus} spec.` +
377
+ (notes ? `\n\nExtra guidance:\n${notes}` : ""),
378
+ );
379
+ s.save(s.sessionId);
380
+ });
381
+ })
382
+ .compile();
383
+ ```
384
+
385
+ Run it either way:
386
+
387
+ ```bash
388
+ # Named + flags (scriptable; CI-friendly)
389
+ atomic workflow -n gen-spec -a claude \
390
+ --research_doc=research/docs/2026-04-11-auth.md \
391
+ --focus=standard
392
+
393
+ # Picker (fuzzy-search the workflow list, then fill the form)
394
+ atomic workflow -a claude
395
+ ```
396
+
397
+ </details>
398
+
325
399
  **Key capabilities:**
326
400
 
327
401
  | Capability | Description |
@@ -330,6 +404,8 @@ export default defineWorkflow<"claude">({
330
404
  | **Native TypeScript control flow** | Use `for`, `if/else`, `Promise.all()`, `try/catch` — no framework DSL needed |
331
405
  | **Session return values** | Session callbacks can return data: `const h = await ctx.stage(...); h.result` |
332
406
  | **Transcript passing** | Access prior session output via handle (`s.transcript(handle)`) or name (`s.transcript("name")`) |
407
+ | **Declared input schemas** | Add an `inputs: [...]` array to `defineWorkflow()` and the CLI materialises `--<field>=<value>` flags with built-in validation (required fields, enum membership, unknown flags) |
408
+ | **Interactive picker** | `atomic workflow -a <agent>` launches a fuzzy-searchable picker that renders each workflow's input schema as a form — no flag-memorisation required |
333
409
  | **Nested sub-sessions** | Call `s.stage()` inside a session callback to spawn child sessions — visible as nested nodes in the graph |
334
410
  | **Auto-inferred graph** | Graph topology auto-inferred from `await`/`Promise.all` patterns — no annotations needed |
335
411
  | **Provider-agnostic** | Write raw SDK code for Claude, Copilot, or OpenCode inside each session callback |
@@ -368,7 +444,7 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
368
444
 
369
445
  | Property | Type | Description |
370
446
  | ----------------------- | ------------------------- | -------------------------------------------------------------- |
371
- | `ctx.userPrompt` | `string` | Original user prompt from the CLI invocation |
447
+ | `ctx.inputs` | `Record<string, string>` | Structured inputs for this run. Free-form workflows store their positional prompt under `ctx.inputs.prompt`; workflows with a declared `inputs` schema store one key per declared field |
372
448
  | `ctx.agent` | `AgentType` | Which agent is running (`"claude"`, `"copilot"`, `"opencode"`) |
373
449
  | `ctx.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a session — returns handle with `name`, `id`, `result` |
374
450
  | `ctx.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript (`{ path, content }`) |
@@ -380,7 +456,7 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
380
456
  | ----------------------- | ------------------------- | -------------------------------------------------------------- |
381
457
  | `s.client` | `ProviderClient<A>` | Pre-created SDK client (auto-managed by runtime) |
382
458
  | `s.session` | `ProviderSession<A>` | Pre-created provider session (auto-managed by runtime) |
383
- | `s.userPrompt` | `string` | Original user prompt from the CLI invocation |
459
+ | `s.inputs` | `Record<string, string>` | Same inputs record as `ctx.inputs`, forwarded into every stage so session callbacks can read values without closing over the outer `ctx` |
384
460
  | `s.agent` | `AgentType` | Which agent is running |
385
461
  | `s.paneId` | `string` | tmux pane ID for this session |
386
462
  | `s.sessionId` | `string` | Session UUID |
@@ -750,12 +826,35 @@ atomic chat -a claude --verbose # Forward --verbose to claude
750
826
 
751
827
  #### `atomic workflow` Flags
752
828
 
753
- | Flag | Description |
754
- | -------------------- | ------------------------------------------------------------------- |
755
- | `-n, --name <name>` | Workflow name (matches directory under `.atomic/workflows/<name>/`) |
756
- | `-a, --agent <name>` | Agent: `claude`, `opencode`, `copilot` |
757
- | `-l, --list` | List available workflows |
758
- | `[prompt...]` | Prompt for the workflow |
829
+ | Flag | Description |
830
+ | -------------------------- | ------------------------------------------------------------------- |
831
+ | `-n, --name <name>` | Workflow name (matches directory under `.atomic/workflows/<name>/`) |
832
+ | `-a, --agent <name>` | Agent: `claude`, `opencode`, `copilot` |
833
+ | `-l, --list` | List available workflows, grouped by source |
834
+ | `--<field>=<value>` | Structured input for workflows that declare an `inputs` schema (also accepts `--<field> <value>`) |
835
+ | `[prompt...]` | Positional prompt for free-form workflows (rejected on workflows with a declared schema) |
836
+
837
+ The workflow command supports four invocation shapes:
838
+
839
+ ```bash
840
+ # 1. List every workflow available to you, grouped by source
841
+ atomic workflow -l
842
+
843
+ # 2. Launch the interactive picker for an agent (no -n) — fuzzy-search
844
+ # the list, fill the form rendered from the workflow's declared inputs,
845
+ # and confirm with y/n
846
+ atomic workflow -a claude
847
+
848
+ # 3. Run a free-form workflow with a positional prompt
849
+ atomic workflow -n ralph -a claude "build a REST API for user management"
850
+
851
+ # 4. Run a structured-input workflow with one --<field> flag per declared input
852
+ atomic workflow -n gen-spec -a claude \
853
+ --research_doc=research/docs/2026-04-11-auth.md \
854
+ --focus=standard
855
+ ```
856
+
857
+ Workflows that declare an `inputs: WorkflowInput[]` schema get CLI flag validation for free — missing required fields and invalid enum values are rejected before any tmux session is spawned, with error messages that spell out the expected flag set. Workflows that don't declare a schema still accept a single positional prompt, which the runtime stores under `ctx.inputs.prompt`. **Builtin workflows (like `ralph`) are reserved names** — a local or global workflow with the same name will not shadow a builtin at resolution time.
759
858
 
760
859
  ### Atomic-Provided Skills (invokable from any agent chat)
761
860
 
@@ -1,5 +1,22 @@
1
1
  // @bun
2
2
  // src/sdk/define-workflow.ts
3
+ function validateWorkflowInput(input, workflowName) {
4
+ if (!input.name || input.name.trim() === "") {
5
+ throw new Error(`Workflow "${workflowName}" has an input with an empty name.`);
6
+ }
7
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(input.name)) {
8
+ throw new Error(`Workflow "${workflowName}" input "${input.name}" has an invalid ` + `name \u2014 must start with a letter and contain only letters, ` + `digits, underscores, and dashes (so it can be used as a ` + `\`--${input.name}\` CLI flag).`);
9
+ }
10
+ if (input.type === "enum") {
11
+ if (!Array.isArray(input.values) || input.values.length === 0) {
12
+ throw new Error(`Workflow "${workflowName}" input "${input.name}" is an enum but ` + `declares no \`values\`.`);
13
+ }
14
+ if (input.default !== undefined && !input.values.includes(input.default)) {
15
+ throw new Error(`Workflow "${workflowName}" input "${input.name}" has a default ` + `"${input.default}" that is not one of its declared values: ` + `${input.values.join(", ")}.`);
16
+ }
17
+ }
18
+ }
19
+
3
20
  class WorkflowBuilder {
4
21
  __brand = "WorkflowBuilder";
5
22
  options;
@@ -22,10 +39,21 @@ class WorkflowBuilder {
22
39
  throw new Error(`Workflow "${this.options.name}" has no run callback. ` + `Add a .run(async (ctx) => { ... }) call before .compile().`);
23
40
  }
24
41
  const runFn = this.runFn;
42
+ const declaredInputs = this.options.inputs ?? [];
43
+ const seen = new Set;
44
+ for (const input of declaredInputs) {
45
+ validateWorkflowInput(input, this.options.name);
46
+ if (seen.has(input.name)) {
47
+ throw new Error(`Workflow "${this.options.name}" has duplicate input name "${input.name}".`);
48
+ }
49
+ seen.add(input.name);
50
+ }
51
+ const inputs = Object.freeze(declaredInputs.map((i) => Object.freeze({ ...i })));
25
52
  return {
26
53
  __brand: "WorkflowDefinition",
27
54
  name: this.options.name,
28
55
  description: this.options.description ?? "",
56
+ inputs,
29
57
  run: runFn
30
58
  };
31
59
  }
@@ -37,133 +65,6 @@ function defineWorkflow(options) {
37
65
  return new WorkflowBuilder(options);
38
66
  }
39
67
 
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
68
  // src/sdk/providers/copilot.ts
168
69
  function validateCopilotWorkflow(source) {
169
70
  const warnings = [];
@@ -201,6 +102,9 @@ function validateOpenCodeWorkflow(source) {
201
102
  }
202
103
 
203
104
  // src/sdk/runtime/tmux.ts
105
+ import { join } from "path";
106
+ var SOCKET_NAME = "atomic";
107
+ var CONFIG_PATH = join(import.meta.dir, "tmux.conf");
204
108
  var resolvedMuxBinary;
205
109
  function getMuxBinary() {
206
110
  if (resolvedMuxBinary !== undefined)
@@ -232,8 +136,9 @@ function tmuxRun(args) {
232
136
  if (!binary) {
233
137
  return { ok: false, stderr: "No terminal multiplexer (tmux/psmux) found on PATH" };
234
138
  }
139
+ const fullArgs = ["-f", CONFIG_PATH, "-L", SOCKET_NAME, ...args];
235
140
  const result = Bun.spawnSync({
236
- cmd: [binary, ...args],
141
+ cmd: [binary, ...fullArgs],
237
142
  stdout: "pipe",
238
143
  stderr: "pipe"
239
144
  });
@@ -363,15 +268,8 @@ function killWindow(sessionName, windowName) {
363
268
  } catch {}
364
269
  }
365
270
  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;
271
+ const result = tmuxRun(["has-session", "-t", sessionName]);
272
+ return result.ok;
375
273
  }
376
274
  function attachSession(sessionName) {
377
275
  const binary = getMuxBinary();
@@ -379,7 +277,7 @@ function attachSession(sessionName) {
379
277
  throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
380
278
  }
381
279
  const proc = Bun.spawnSync({
382
- cmd: [binary, "attach-session", "-t", sessionName],
280
+ cmd: [binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName],
383
281
  stdin: "inherit",
384
282
  stdout: "inherit",
385
283
  stderr: "pipe"
@@ -389,6 +287,13 @@ function attachSession(sessionName) {
389
287
  throw new Error(`Failed to attach to session: ${sessionName}${stderr ? ` (${stderr})` : ""}`);
390
288
  }
391
289
  }
290
+ function spawnMuxAttach(sessionName) {
291
+ const binary = getMuxBinary();
292
+ if (!binary) {
293
+ throw new Error("No terminal multiplexer (tmux/psmux) found on PATH");
294
+ }
295
+ return Bun.spawn([binary, "-f", CONFIG_PATH, "-L", SOCKET_NAME, "attach-session", "-t", sessionName], { stdio: ["inherit", "inherit", "inherit"] });
296
+ }
392
297
  function switchClient(sessionName) {
393
298
  tmuxExec(["switch-client", "-t", sessionName]);
394
299
  }
@@ -741,8 +646,156 @@ var WorkflowLoader;
741
646
  WorkflowLoader.loadWorkflow = loadWorkflow;
742
647
  })(WorkflowLoader ||= {});
743
648
 
649
+ // src/sdk/runtime/discovery.ts
650
+ import { join as join2 } from "path";
651
+ import { readdir, writeFile } from "fs/promises";
652
+ import { existsSync, readdirSync } from "fs";
653
+ import { homedir } from "os";
654
+ import ignore from "ignore";
655
+ function getLocalWorkflowsDir(projectRoot) {
656
+ return join2(projectRoot, ".atomic", "workflows");
657
+ }
658
+ function getGlobalWorkflowsDir() {
659
+ return join2(homedir(), ".atomic", "workflows");
660
+ }
661
+ var AGENTS = ["copilot", "opencode", "claude"];
662
+ var AGENT_SET = new Set(AGENTS);
663
+ var WORKFLOWS_GITIGNORE = [
664
+ "node_modules/",
665
+ "dist/",
666
+ "build/",
667
+ "coverage/",
668
+ ".cache/",
669
+ "*.log",
670
+ "*.tsbuildinfo",
671
+ ""
672
+ ].join(`
673
+ `);
674
+ async function loadWorkflowsGitignore(workflowsDir) {
675
+ const gitignorePath = join2(workflowsDir, ".gitignore");
676
+ let content;
677
+ try {
678
+ content = await Bun.file(gitignorePath).text();
679
+ } catch {
680
+ await writeFile(gitignorePath, WORKFLOWS_GITIGNORE);
681
+ content = WORKFLOWS_GITIGNORE;
682
+ }
683
+ return ignore().add(content);
684
+ }
685
+ async function discoverFromBaseDir(baseDir, source, agentFilter) {
686
+ const workflows = [];
687
+ const agents = agentFilter ? [agentFilter] : AGENTS;
688
+ const agentNames = new Set(agents);
689
+ let workflowEntries;
690
+ try {
691
+ workflowEntries = await readdir(baseDir, { withFileTypes: true });
692
+ } catch {
693
+ return workflows;
694
+ }
695
+ const ig = await loadWorkflowsGitignore(baseDir);
696
+ for (const wfEntry of workflowEntries) {
697
+ if (!wfEntry.isDirectory())
698
+ continue;
699
+ if (wfEntry.name.startsWith("."))
700
+ continue;
701
+ if (AGENT_SET.has(wfEntry.name))
702
+ continue;
703
+ if (ig.ignores(wfEntry.name + "/"))
704
+ continue;
705
+ const workflowDir = join2(baseDir, wfEntry.name);
706
+ let agentEntries;
707
+ try {
708
+ agentEntries = await readdir(workflowDir, { withFileTypes: true });
709
+ } catch {
710
+ continue;
711
+ }
712
+ for (const agentEntry of agentEntries) {
713
+ if (!agentEntry.isDirectory())
714
+ continue;
715
+ if (!agentNames.has(agentEntry.name))
716
+ continue;
717
+ const indexPath = join2(workflowDir, agentEntry.name, "index.ts");
718
+ const file = Bun.file(indexPath);
719
+ if (await file.exists()) {
720
+ workflows.push({
721
+ name: wfEntry.name,
722
+ agent: agentEntry.name,
723
+ path: indexPath,
724
+ source
725
+ });
726
+ }
727
+ }
728
+ }
729
+ return workflows;
730
+ }
731
+ var BUILTIN_WORKFLOWS_DIR = join2(Bun.fileURLToPath(new URL("../workflows/builtin", import.meta.url)));
732
+ function discoverBuiltinWorkflows(agentFilter) {
733
+ const results = [];
734
+ const agents = agentFilter ? [agentFilter] : AGENTS;
735
+ let workflowNames;
736
+ try {
737
+ workflowNames = readdirSync(BUILTIN_WORKFLOWS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
738
+ } catch {
739
+ return results;
740
+ }
741
+ for (const name of workflowNames) {
742
+ for (const agent of agents) {
743
+ const indexPath = join2(BUILTIN_WORKFLOWS_DIR, name, agent, "index.ts");
744
+ if (existsSync(indexPath)) {
745
+ results.push({ name, agent, path: indexPath, source: "builtin" });
746
+ }
747
+ }
748
+ }
749
+ return results;
750
+ }
751
+ async function discoverWorkflows(projectRoot = process.cwd(), agentFilter, options = {}) {
752
+ const { merge = true } = options;
753
+ const localDir = getLocalWorkflowsDir(projectRoot);
754
+ const globalDir = getGlobalWorkflowsDir();
755
+ const allBuiltins = discoverBuiltinWorkflows();
756
+ const reservedNames = new Set(allBuiltins.map((w) => w.name));
757
+ const builtinResults = agentFilter ? allBuiltins.filter((w) => w.agent === agentFilter) : allBuiltins;
758
+ const [globalResults, localResults] = await Promise.all([
759
+ discoverFromBaseDir(globalDir, "global", agentFilter),
760
+ discoverFromBaseDir(localDir, "local", agentFilter)
761
+ ]);
762
+ const filteredGlobal = globalResults.filter((w) => !reservedNames.has(w.name));
763
+ const filteredLocal = localResults.filter((w) => !reservedNames.has(w.name));
764
+ if (!merge) {
765
+ return [...filteredGlobal, ...filteredLocal, ...builtinResults];
766
+ }
767
+ const byKey = new Map;
768
+ for (const wf of filteredGlobal) {
769
+ byKey.set(`${wf.agent}/${wf.name}`, wf);
770
+ }
771
+ for (const wf of filteredLocal) {
772
+ byKey.set(`${wf.agent}/${wf.name}`, wf);
773
+ }
774
+ for (const wf of builtinResults) {
775
+ byKey.set(`${wf.agent}/${wf.name}`, wf);
776
+ }
777
+ return Array.from(byKey.values());
778
+ }
779
+ async function findWorkflow(name, agent, projectRoot = process.cwd()) {
780
+ const all = await discoverWorkflows(projectRoot, agent);
781
+ return all.find((w) => w.name === name) ?? null;
782
+ }
783
+ async function loadWorkflowsMetadata(discovered) {
784
+ const results = await Promise.all(discovered.map(async (wf) => {
785
+ const loaded = await WorkflowLoader.loadWorkflow(wf);
786
+ if (!loaded.ok)
787
+ return null;
788
+ return {
789
+ ...wf,
790
+ description: loaded.value.definition.description,
791
+ inputs: loaded.value.definition.inputs
792
+ };
793
+ }));
794
+ return results.filter((r) => r !== null);
795
+ }
796
+
744
797
  // src/sdk/runtime/executor.ts
745
- import { join as join2, resolve } from "path";
798
+ import { join as join3, resolve } from "path";
746
799
  import { homedir as homedir2 } from "os";
747
800
  import { mkdir, writeFile as writeFile2, readFile } from "fs/promises";
748
801
 
@@ -915,7 +968,7 @@ function generateId() {
915
968
  return crypto.randomUUID().slice(0, 8);
916
969
  }
917
970
  function getSessionsBaseDir() {
918
- return join2(homedir2(), ".atomic", "sessions");
971
+ return join3(homedir2(), ".atomic", "sessions");
919
972
  }
920
973
  async function ensureDir(dir) {
921
974
  await mkdir(dir, { recursive: true });
@@ -930,25 +983,26 @@ async function executeWorkflow(options) {
930
983
  const {
931
984
  definition,
932
985
  agent,
933
- prompt,
986
+ inputs = {},
934
987
  workflowFile,
935
988
  projectRoot = process.cwd()
936
989
  } = options;
937
990
  const workflowRunId = generateId();
938
991
  const tmuxSessionName = `atomic-wf-${definition.name}-${workflowRunId}`;
939
- const sessionsBaseDir = join2(getSessionsBaseDir(), workflowRunId);
992
+ const sessionsBaseDir = join3(getSessionsBaseDir(), workflowRunId);
940
993
  await ensureDir(sessionsBaseDir);
941
994
  const thisFile = resolve(import.meta.dir, "executor-entry.ts");
942
995
  const isWin = process.platform === "win32";
943
996
  const launcherExt = isWin ? "ps1" : "sh";
944
- const launcherPath = join2(sessionsBaseDir, `orchestrator.${launcherExt}`);
945
- const logPath = join2(sessionsBaseDir, "orchestrator.log");
997
+ const launcherPath = join3(sessionsBaseDir, `orchestrator.${launcherExt}`);
998
+ const logPath = join3(sessionsBaseDir, "orchestrator.log");
999
+ const inputsB64 = Buffer.from(JSON.stringify(inputs)).toString("base64");
946
1000
  const launcherScript = isWin ? [
947
1001
  `Set-Location "${escPwsh(projectRoot)}"`,
948
1002
  `$env:ATOMIC_WF_ID = "${escPwsh(workflowRunId)}"`,
949
1003
  `$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
950
1004
  `$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
951
- `$env:ATOMIC_WF_PROMPT = "${escPwsh(Buffer.from(prompt).toString("base64"))}"`,
1005
+ `$env:ATOMIC_WF_INPUTS = "${escPwsh(inputsB64)}"`,
952
1006
  `$env:ATOMIC_WF_FILE = "${escPwsh(workflowFile)}"`,
953
1007
  `$env:ATOMIC_WF_CWD = "${escPwsh(projectRoot)}"`,
954
1008
  `bun run "${escPwsh(thisFile)}" 2>"${escPwsh(logPath)}"`
@@ -959,24 +1013,29 @@ async function executeWorkflow(options) {
959
1013
  `export ATOMIC_WF_ID="${escBash(workflowRunId)}"`,
960
1014
  `export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
961
1015
  `export ATOMIC_WF_AGENT="${escBash(agent)}"`,
962
- `export ATOMIC_WF_PROMPT="${escBash(Buffer.from(prompt).toString("base64"))}"`,
1016
+ `export ATOMIC_WF_INPUTS="${escBash(inputsB64)}"`,
963
1017
  `export ATOMIC_WF_FILE="${escBash(workflowFile)}"`,
964
1018
  `export ATOMIC_WF_CWD="${escBash(projectRoot)}"`,
965
1019
  `bun run "${escBash(thisFile)}" 2>"${escBash(logPath)}"`
966
1020
  ].join(`
967
1021
  `);
968
1022
  await writeFile2(launcherPath, launcherScript, { mode: 493 });
969
- const shellCmd = isWin ? `pwsh -NoProfile -File "${escPwsh(launcherPath)}"` : `bash "${escBash(launcherPath)}"`;
970
- createSession(tmuxSessionName, shellCmd, "orchestrator");
1023
+ console.log(`[atomic] Session: ${tmuxSessionName} (FYI all atomic sessions run on tmux -L ${SOCKET_NAME})`);
971
1024
  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"]
1025
+ const defaultShell = process.env.SHELL || (isWin ? "pwsh" : "sh");
1026
+ createSession(tmuxSessionName, defaultShell, "orchestrator");
1027
+ const launcherCmd = isWin ? ["pwsh", "-NoProfile", "-File", launcherPath] : ["bash", launcherPath];
1028
+ const proc = Bun.spawn(launcherCmd, {
1029
+ stdio: ["inherit", "inherit", "inherit"],
1030
+ cwd: projectRoot
977
1031
  });
1032
+ await proc.exited;
1033
+ } else {
1034
+ const shellCmd = isWin ? `pwsh -NoProfile -File "${escPwsh(launcherPath)}"` : `bash "${escBash(launcherPath)}"`;
1035
+ createSession(tmuxSessionName, shellCmd, "orchestrator");
1036
+ const attachProc = spawnMuxAttach(tmuxSessionName);
978
1037
  await attachProc.exited;
979
1038
  }
980
1039
  }
981
1040
 
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 };
1041
+ export { WorkflowBuilder, defineWorkflow, validateCopilotWorkflow, validateOpenCodeWorkflow, SOCKET_NAME, getMuxBinary, resetMuxBinaryCache, isTmuxInstalled, isInsideTmux, tmuxRun, createSession, createWindow, createPane, sendLiteralText, sendSpecialKey, sendKeysAndSubmit, capturePane, capturePaneVisible, capturePaneScrollback, killSession, killWindow, sessionExists, attachSession, spawnMuxAttach, switchClient, getCurrentSession, attachOrSwitch, selectWindow, normalizeTmuxCapture, normalizeTmuxLines, paneLooksReady, paneHasActiveTask, paneIsIdle, waitForPaneReady, attemptSubmitRounds, waitForOutput, clearClaudeSession, createClaudeSession, claudeQuery, validateClaudeWorkflow, WorkflowLoader, AGENTS, WORKFLOWS_GITIGNORE, discoverWorkflows, findWorkflow, loadWorkflowsMetadata, executeWorkflow };