@bastani/atomic 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +79 -183
  2. package/package.json +6 -4
  3. package/src/commands/cli/chat/index.ts +2 -2
  4. package/src/commands/cli/workflow.ts +6 -6
  5. package/src/scripts/constants.ts +2 -2
  6. package/src/sdk/components/session-graph-panel.tsx +1 -3
  7. package/src/sdk/runtime/discovery.ts +51 -3
  8. package/src/sdk/runtime/executor-entry.ts +16 -0
  9. package/src/sdk/runtime/executor.ts +31 -17
  10. package/src/sdk/runtime/loader.ts +0 -127
  11. package/{.atomic/workflows → src/sdk/workflows/builtin}/ralph/claude/index.ts +1 -1
  12. package/{.atomic/workflows → src/sdk/workflows/builtin}/ralph/copilot/index.ts +1 -1
  13. package/{.atomic/workflows → src/sdk/workflows/builtin}/ralph/opencode/index.ts +1 -1
  14. package/src/sdk/{workflows.ts → workflows/index.ts} +14 -14
  15. package/src/services/system/auto-sync.ts +0 -2
  16. package/.atomic/workflows/hello/claude/index.ts +0 -40
  17. package/.atomic/workflows/hello/copilot/index.ts +0 -53
  18. package/.atomic/workflows/hello/opencode/index.ts +0 -52
  19. package/.atomic/workflows/hello-parallel/claude/index.ts +0 -73
  20. package/.atomic/workflows/hello-parallel/copilot/index.ts +0 -103
  21. package/.atomic/workflows/hello-parallel/opencode/index.ts +0 -105
  22. package/.atomic/workflows/tsconfig.json +0 -22
  23. package/src/services/system/workflows.ts +0 -240
  24. /package/{.atomic/workflows → src/sdk/workflows/builtin}/ralph/helpers/git.ts +0 -0
  25. /package/{.atomic/workflows → src/sdk/workflows/builtin}/ralph/helpers/prompts.ts +0 -0
  26. /package/{.atomic/workflows → src/sdk/workflows/builtin}/ralph/helpers/review.ts +0 -0
package/README.md CHANGED
@@ -123,57 +123,6 @@ Windows PowerShell 7+:
123
123
  irm https://raw.githubusercontent.com/flora131/atomic/main/install.ps1 | iex
124
124
  ```
125
125
 
126
- <details>
127
- <summary>Migrating from v0.4.x (Binary) to v0.5.x (npm)?</summary>
128
-
129
- Atomic has moved from a standalone binary distribution to an **npm package**. The new version gives you the Workflow SDK, 58 skills, and 12 sub-agents as a single installable package.
130
-
131
- #### Migration Steps
132
-
133
- **1. Uninstall the old binary:**
134
-
135
- ```bash
136
- atomic uninstall
137
- ```
138
-
139
- **2. Remove the old Workflow SDK global package:**
140
-
141
- ```bash
142
- bun uninstall -g @bastani/atomic-workflows
143
- ```
144
-
145
- **3. Delete the old configuration directory:**
146
-
147
- ```bash
148
- rm -rf ~/.atomic
149
- ```
150
-
151
- **4. Install the new version:**
152
-
153
- ```bash
154
- bun install -g @bastani/atomic
155
- ```
156
-
157
- **5. Re-initialize your project:**
158
-
159
- ```bash
160
- cd your-project
161
- atomic init
162
- ```
163
-
164
- > On first run after install, Atomic automatically syncs all agent configurations, skills, workflows, and tooling. This replaces the old `atomic update` command — updates now happen lazily on CLI startup when a version mismatch is detected.
165
-
166
- #### What Changed
167
-
168
- | Aspect | v0.4.x (Binary) | v0.5.x (npm) |
169
- | --- | --- | --- |
170
- | **Distribution** | Pre-compiled binary via `install.sh` | npm package via `bun install -g` |
171
- | **Updates** | `atomic update` command | Reinstall via `bun install -g @bastani/atomic` + auto-sync on first run |
172
- | **Uninstall** | `atomic uninstall` | `bun uninstall -g @bastani/atomic` |
173
- | **Workflow SDK** | Separate `@bastani/atomic-workflows` global package | Bundled with CLI as workspace package |
174
- | **Config sync** | Manual via install scripts | Automatic on first run after upgrade |
175
-
176
- </details>
177
126
 
178
127
  ### 2. Initialize Your Project
179
128
 
@@ -202,56 +151,50 @@ This explores your codebase using sub-agents and generates documentation that gi
202
151
 
203
152
  Every team has a process. Atomic lets you encode it as TypeScript — chain agent sessions together, pass transcripts between them, and run the whole thing from the CLI.
204
153
 
205
- Drop a `.ts` file in `.atomic/workflows/<name>/<agent>/index.ts` and run it:
154
+ Create a workflow project, install the SDK, and add your workflow file:
206
155
 
207
156
  ```bash
208
- atomic workflow -n my-workflow -a claude "add user avatars to the profile page"
157
+ bun init && bun add @bastani/atomic
158
+ mkdir -p .atomic/workflows/my-workflow/claude
209
159
  ```
210
160
 
211
- Here's a workflow that researches a codebase, implements a feature, and reviews the result — three sessions, each in its own context window:
212
-
213
161
  ```ts
214
162
  // .atomic/workflows/my-workflow/claude/index.ts
215
- import { defineWorkflow, createClaudeSession, claudeQuery } from "@bastani/atomic/workflows";
163
+ import { defineWorkflow } from "@bastani/atomic/workflows";
216
164
 
217
- export default defineWorkflow({
165
+ export default defineWorkflow<"claude">({
218
166
  name: "my-workflow",
219
167
  description: "Research -> Implement -> Review",
220
168
  })
221
169
  .run(async (ctx) => {
222
- const research = await ctx.session(
223
- { name: "research", description: "Analyze the codebase for the requested change" },
170
+ const research = await ctx.stage(
171
+ { name: "research", description: "Analyze the codebase" },
172
+ {}, {},
224
173
  async (s) => {
225
- await createClaudeSession({ paneId: s.paneId });
226
- await claudeQuery({
227
- paneId: s.paneId,
228
- prompt: `/research-codebase ${s.userPrompt}`,
229
- });
174
+ await s.session.query(`/research-codebase ${s.userPrompt}`);
230
175
  s.save(s.sessionId);
231
176
  },
232
177
  );
233
178
 
234
- const implement = await ctx.session(
235
- { name: "implement", description: "Implement the feature based on research findings" },
179
+ await ctx.stage(
180
+ { name: "implement", description: "Implement based on research" },
181
+ {}, {},
236
182
  async (s) => {
237
183
  const transcript = await s.transcript(research);
238
- await createClaudeSession({ paneId: s.paneId });
239
- await claudeQuery({
240
- paneId: s.paneId,
241
- prompt: `Read ${transcript.path} and implement the changes described. Run tests to verify.`,
242
- });
184
+ await s.session.query(
185
+ `Read ${transcript.path} and implement the changes. Run tests to verify.`,
186
+ );
243
187
  s.save(s.sessionId);
244
188
  },
245
189
  );
246
190
 
247
- await ctx.session(
248
- { name: "review", description: "Review the implementation for correctness" },
191
+ await ctx.stage(
192
+ { name: "review", description: "Review the implementation" },
193
+ {}, {},
249
194
  async (s) => {
250
- await createClaudeSession({ paneId: s.paneId });
251
- await claudeQuery({
252
- paneId: s.paneId,
253
- prompt: "Review all uncommitted changes. Flag any issues with correctness, tests, or style.",
254
- });
195
+ await s.session.query(
196
+ "Review all uncommitted changes. Flag any issues with correctness, tests, or style.",
197
+ );
255
198
  s.save(s.sessionId);
256
199
  },
257
200
  );
@@ -259,7 +202,13 @@ export default defineWorkflow({
259
202
  .compile();
260
203
  ```
261
204
 
262
- This is just one example. Add a spec phase, parallelize independent sessions, swap in a different agent — the workflow is yours to define. See [Workflow SDK — Build Your Own Harness](#workflow-sdk--build-your-own-harness) for the full API and more examples.
205
+ Run it:
206
+
207
+ ```bash
208
+ atomic workflow -n my-workflow -a claude "add user avatars to the profile page"
209
+ ```
210
+
211
+ Add a spec phase, parallelize independent sessions, swap in a different agent — the workflow is yours to define. See [Workflow SDK — Build Your Own Harness](#workflow-sdk--build-your-own-harness) for the full API and more examples.
263
212
 
264
213
  > **Want something that works out of the box?** Atomic ships with `ralph`, a built-in workflow that plans, implements, reviews, and debugs autonomously — see [Autonomous Execution (Ralph)](#autonomous-execution-ralph).
265
214
 
@@ -283,42 +232,41 @@ Each agent gets its own configuration directory (`.claude/`, `.opencode/`, `.git
283
232
 
284
233
  Every team has a process — triage bugs this way, ship features that way, review PRs with these checks. Most of it lives in a wiki nobody reads or in one senior engineer's head. The **Workflow SDK** (`@bastani/atomic/workflows`) lets you encode that process as TypeScript — spawn agent sessions dynamically with native control flow (`for`, `if`, `Promise.all()`), and watch them appear in a live graph as they execute.
285
234
 
286
- Drop a `.ts` file in `.atomic/workflows/<name>/<agent>/index.ts` and run it:
235
+ Set up a workflow project (`bun init && bun add @bastani/atomic`), create a `.ts` file in `.atomic/workflows/<name>/<agent>/index.ts`, and run it:
287
236
 
288
237
  ```bash
289
- atomic workflow -n hello -a claude "describe this project"
238
+ atomic workflow -n my-workflow -a claude "describe this project"
290
239
  ```
291
240
 
292
241
  <details>
293
242
  <summary>Example: Sequential workflow (describe -> summarize)</summary>
294
243
 
295
244
  ```ts
296
- // .atomic/workflows/hello/claude/index.ts
297
- import { defineWorkflow, createClaudeSession, claudeQuery } from "@bastani/atomic/workflows";
245
+ // .atomic/workflows/my-workflow/claude/index.ts
246
+ import { defineWorkflow } from "@bastani/atomic/workflows";
298
247
 
299
- export default defineWorkflow({
300
- name: "hello",
301
- description: "Two-session Claude demo: describe -> summarize",
248
+ export default defineWorkflow<"claude">({
249
+ name: "my-workflow",
250
+ description: "Two-session pipeline: describe -> summarize",
302
251
  })
303
252
  .run(async (ctx) => {
304
253
  const describe = await ctx.stage(
305
254
  { name: "describe", description: "Ask Claude to describe the project" },
255
+ {}, {},
306
256
  async (s) => {
307
- await createClaudeSession({ paneId: s.paneId });
308
- await claudeQuery({ paneId: s.paneId, prompt: s.userPrompt });
257
+ await s.session.query(s.userPrompt);
309
258
  s.save(s.sessionId);
310
259
  },
311
260
  );
312
261
 
313
262
  await ctx.stage(
314
263
  { name: "summarize", description: "Summarize the previous session's output" },
264
+ {}, {},
315
265
  async (s) => {
316
266
  const research = await s.transcript(describe);
317
- await createClaudeSession({ paneId: s.paneId });
318
- await claudeQuery({
319
- paneId: s.paneId,
320
- prompt: `Read ${research.path} and summarize it in 2-3 bullet points.`,
321
- });
267
+ await s.session.query(
268
+ `Read ${research.path} and summarize it in 2-3 bullet points.`,
269
+ );
322
270
  s.save(s.sessionId);
323
271
  },
324
272
  );
@@ -332,71 +280,42 @@ export default defineWorkflow({
332
280
  <summary>Example: Parallel workflow (describe -> [summarize-a, summarize-b] -> merge)</summary>
333
281
 
334
282
  ```ts
335
- // .atomic/workflows/hello-parallel/claude/index.ts
336
- import { defineWorkflow, createClaudeSession, claudeQuery } from "@bastani/atomic/workflows";
283
+ import { defineWorkflow } from "@bastani/atomic/workflows";
337
284
 
338
- export default defineWorkflow({
339
- name: "hello-parallel",
340
- description: "Parallel Claude demo: describe -> [summarize-a, summarize-b] -> merge",
285
+ export default defineWorkflow<"claude">({
286
+ name: "parallel-demo",
287
+ description: "describe -> [summarize-a, summarize-b] -> merge",
341
288
  })
342
289
  .run(async (ctx) => {
343
- const describe = await ctx.session(
344
- { name: "describe", description: "Ask Claude to describe the project" },
290
+ const describe = await ctx.stage(
291
+ { name: "describe" }, {}, {},
345
292
  async (s) => {
346
- await createClaudeSession({ paneId: s.paneId });
347
- await claudeQuery({ paneId: s.paneId, prompt: s.userPrompt });
293
+ await s.session.query(s.userPrompt);
348
294
  s.save(s.sessionId);
349
295
  },
350
296
  );
351
297
 
352
298
  const [summarizeA, summarizeB] = await Promise.all([
353
- ctx.session(
354
- { name: "summarize-a", description: "Summarize the description as bullet points" },
355
- async (s) => {
356
- const research = await s.transcript(describe);
357
- await createClaudeSession({ paneId: s.paneId });
358
- await claudeQuery({
359
- paneId: s.paneId,
360
- prompt: `Read ${research.path} and summarize it in 2-3 bullet points.`,
361
- });
362
- s.save(s.sessionId);
363
- },
364
- ),
365
- ctx.session(
366
- { name: "summarize-b", description: "Summarize the description as a one-liner" },
367
- async (s) => {
368
- const research = await s.transcript(describe);
369
- await createClaudeSession({ paneId: s.paneId });
370
- await claudeQuery({
371
- paneId: s.paneId,
372
- prompt: `Read ${research.path} and summarize it in a single sentence.`,
373
- });
374
- s.save(s.sessionId);
375
- },
376
- ),
299
+ ctx.stage({ name: "summarize-a" }, {}, {}, async (s) => {
300
+ const research = await s.transcript(describe);
301
+ await s.session.query(`Read ${research.path} and summarize in 2-3 bullet points.`);
302
+ s.save(s.sessionId);
303
+ }),
304
+ ctx.stage({ name: "summarize-b" }, {}, {}, async (s) => {
305
+ const research = await s.transcript(describe);
306
+ await s.session.query(`Read ${research.path} and summarize in a single sentence.`);
307
+ s.save(s.sessionId);
308
+ }),
377
309
  ]);
378
310
 
379
- await ctx.session(
380
- { name: "merge", description: "Merge both summaries into a final output" },
381
- async (s) => {
382
- const bullets = await s.transcript(summarizeA);
383
- const oneliner = await s.transcript(summarizeB);
384
- await createClaudeSession({ paneId: s.paneId });
385
- await claudeQuery({
386
- paneId: s.paneId,
387
- prompt: [
388
- "Combine the following two summaries into one concise paragraph:",
389
- "",
390
- "## Bullet points",
391
- bullets.content,
392
- "",
393
- "## One-liner",
394
- oneliner.content,
395
- ].join("\n"),
396
- });
397
- s.save(s.sessionId);
398
- },
399
- );
311
+ await ctx.stage({ name: "merge" }, {}, {}, async (s) => {
312
+ const bullets = await s.transcript(summarizeA);
313
+ const oneliner = await s.transcript(summarizeB);
314
+ await s.session.query(
315
+ `Combine:\n\n## Bullets\n${bullets.content}\n\n## One-liner\n${oneliner.content}`,
316
+ );
317
+ s.save(s.sessionId);
318
+ });
400
319
  })
401
320
  .compile();
402
321
  ```
@@ -428,7 +347,7 @@ Workflows are deterministic by design — the same definition always produces th
428
347
 
429
348
  This means you can run the same workflow on different machines, different agents, or at different times and get structurally identical execution — same steps, same data flow, same ordering. The only variance comes from the LLM's responses, not from the harness.
430
349
 
431
- Drop a `.ts` file in `.atomic/workflows/<name>/<agent>/` (project-local) or `~/.atomic/workflows/` (global). You can also ask Atomic to create workflows for you:
350
+ Set up a project (`bun init && bun add @bastani/atomic`), drop a `.ts` file in `.atomic/workflows/<name>/<agent>/index.ts`, and run it. You can also ask Atomic to create workflows for you:
432
351
 
433
352
  ```
434
353
  Use your workflow-creator skill to create a workflow that plans, implements, and reviews a feature.
@@ -451,7 +370,7 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
451
370
  | ----------------------- | ------------------------- | -------------------------------------------------------------- |
452
371
  | `ctx.userPrompt` | `string` | Original user prompt from the CLI invocation |
453
372
  | `ctx.agent` | `AgentType` | Which agent is running (`"claude"`, `"copilot"`, `"opencode"`) |
454
- | `ctx.stage(opts, fn)` | `Promise<SessionHandle<T>>` | Spawn a session — returns handle with `name`, `id`, `result` |
373
+ | `ctx.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a session — returns handle with `name`, `id`, `result` |
455
374
  | `ctx.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript (`{ path, content }`) |
456
375
  | `ctx.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
457
376
 
@@ -459,7 +378,8 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
459
378
 
460
379
  | Property | Type | Description |
461
380
  | ----------------------- | ------------------------- | -------------------------------------------------------------- |
462
- | `s.serverUrl` | `string` | The agent's server URL |
381
+ | `s.client` | `ProviderClient<A>` | Pre-created SDK client (auto-managed by runtime) |
382
+ | `s.session` | `ProviderSession<A>` | Pre-created provider session (auto-managed by runtime) |
463
383
  | `s.userPrompt` | `string` | Original user prompt from the CLI invocation |
464
384
  | `s.agent` | `AgentType` | Which agent is running |
465
385
  | `s.paneId` | `string` | tmux pane ID for this session |
@@ -468,7 +388,7 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
468
388
  | `s.save(messages)` | `SaveTranscript` | Save this session's output for subsequent sessions |
469
389
  | `s.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript |
470
390
  | `s.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
471
- | `s.stage(opts, fn)` | `Promise<SessionHandle<T>>` | Spawn a nested sub-session (child in the graph) |
391
+ | `s.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a nested sub-session (child in the graph) |
472
392
 
473
393
  #### Session Options (`SessionRunOptions`)
474
394
 
@@ -489,38 +409,15 @@ Each provider saves transcripts differently:
489
409
  | **Copilot** | `s.save(await session.getMessages())` — pass `SessionEvent[]` |
490
410
  | **OpenCode** | `s.save(result.data!)` — pass the full `{ info, parts }` response |
491
411
 
492
- #### Provider Helpers
493
-
494
- | Export | Purpose |
495
- | --------------------------------- | --------------------------------------------------- |
496
- | `createClaudeSession(options)` | Start a Claude TUI in a tmux pane |
497
- | `claudeQuery(options)` | Send a prompt to Claude and wait for the response |
498
- | `clearClaudeSession(paneId)` | Free memory for a killed/finished Claude session |
499
- | `validateClaudeWorkflow()` | Validate a Claude workflow source before run |
500
- | `validateCopilotWorkflow()` | Validate a Copilot workflow source before run |
501
- | `validateOpenCodeWorkflow()` | Validate an OpenCode workflow source before run |
502
-
503
- `createClaudeSession` accepts:
412
+ #### Per-Agent Session APIs
504
413
 
505
- | Option | Type | Default | Description |
506
- | ----------------- | ---------- | ----------------------------------------------------- | ---------------------------------- |
507
- | `paneId` | `string` | — | tmux pane ID (required) |
508
- | `chatFlags` | `string[]` | `["--dangerously-skip-permissions"]` | CLI flags passed to `claude` |
509
- | `readyTimeoutMs` | `number` | `30000` | Timeout waiting for TUI readiness |
414
+ The runtime auto-creates `s.client` and `s.session` — use them directly inside the callback:
510
415
 
511
- `claudeQuery` accepts:
512
-
513
- | Option | Type | Default | Description |
514
- | ----------------- | -------- | -------- | ------------------------------------------------ |
515
- | `paneId` | `string` | — | tmux pane ID (required) |
516
- | `prompt` | `string` | — | The prompt to send (required) |
517
- | `timeoutMs` | `number` | `300000` | Response timeout (5 min) |
518
- | `pollIntervalMs` | `number` | `2000` | Polling interval for output stabilization |
519
- | `submitPresses` | `number` | `1` | C-m presses per submit round |
520
- | `maxSubmitRounds` | `number` | `6` | Max retry rounds for delivery confirmation |
521
- | `readyTimeoutMs` | `number` | `30000` | Pane readiness timeout before sending |
522
-
523
- Returns `{ output: string; delivered: boolean }` — `delivered` confirms the prompt was accepted by the agent.
416
+ | Agent | How to send a prompt |
417
+ | ----- | -------------------- |
418
+ | **Claude** | `await s.session.query(prompt)` |
419
+ | **Copilot** | `await s.session.sendAndWait({ prompt }, TIMEOUT_MS)` — explicit timeout required (default 60s throws) |
420
+ | **OpenCode** | `await s.client.session.prompt({ sessionID: s.session.id, parts: [{ type: "text", text: prompt }] })` |
524
421
 
525
422
  #### Key Rules
526
423
 
@@ -529,8 +426,7 @@ Returns `{ output: string; delivered: boolean }` — `delivered` confirms the pr
529
426
  3. `transcript()` / `getMessages()` only access completed sessions (callback returned + saves flushed)
530
427
  4. Each session runs in its own tmux window with the chosen agent
531
428
  5. Workflows are organized per-workflow: `.atomic/workflows/<name>/<agent>/index.ts`
532
-
533
- Workflow files need no `package.json` or `node_modules` of their own — the Atomic loader rewrites `@bastani/atomic/*` and atomic's transitive deps (`@github/copilot-sdk`, `@opencode-ai/sdk`, `@anthropic-ai/claude-agent-sdk`, `zod`, etc.) to absolute paths inside the installed atomic package at load time. Drop a `.ts` file and it runs.
429
+ 6. Set up your workflow project with `bun init && bun add @bastani/atomic` — standard module resolution handles imports
534
430
 
535
431
  For the authoring walkthrough with worked examples, ask Atomic to use the `workflow-creator` skill or read the skill reference at `.agents/skills/workflow-creator/`.
536
432
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,7 +27,10 @@
27
27
  },
28
28
  "exports": {
29
29
  ".": "./src/sdk/index.ts",
30
- "./workflows": "./src/sdk/workflows.ts"
30
+ "./workflows": "./src/sdk/workflows/index.ts",
31
+ "./workflows/builtin/ralph/claude": "./src/sdk/workflows/builtin/ralph/claude/index.ts",
32
+ "./workflows/builtin/ralph/copilot": "./src/sdk/workflows/builtin/ralph/copilot/index.ts",
33
+ "./workflows/builtin/ralph/opencode": "./src/sdk/workflows/builtin/ralph/opencode/index.ts"
31
34
  },
32
35
  "files": [
33
36
  "src",
@@ -36,8 +39,7 @@
36
39
  ".claude/agents",
37
40
  ".opencode/agents",
38
41
  ".github/agents",
39
- ".github/lsp.json",
40
- ".atomic/workflows"
42
+ ".github/lsp.json"
41
43
  ],
42
44
  "scripts": {
43
45
  "dev": "bun run src/cli.ts",
@@ -27,12 +27,12 @@ import {
27
27
  isInsideTmux,
28
28
  isTmuxInstalled,
29
29
  resetMuxBinaryCache,
30
- } from "@/sdk/workflows.ts";
30
+ } from "@/sdk/workflows/index.ts";
31
31
  import {
32
32
  createSession,
33
33
  killSession,
34
34
  getMuxBinary,
35
- } from "@/sdk/workflows.ts";
35
+ } from "@/sdk/workflows/index.ts";
36
36
  import { ensureTmuxInstalled } from "@/lib/spawn.ts";
37
37
 
38
38
  // ============================================================================
@@ -17,8 +17,8 @@ import {
17
17
  executeWorkflow,
18
18
  WorkflowLoader,
19
19
  resetMuxBinaryCache,
20
- } from "@/sdk/workflows.ts";
21
- import type { AgentType, DiscoveredWorkflow } from "@/sdk/workflows.ts";
20
+ } from "@/sdk/workflows/index.ts";
21
+ import type { AgentType, DiscoveredWorkflow } from "@/sdk/workflows/index.ts";
22
22
 
23
23
  export async function workflowCommand(options: {
24
24
  name?: string;
@@ -117,9 +117,7 @@ export async function workflowCommand(options: {
117
117
  }
118
118
 
119
119
  // Load workflow through the pipeline: resolve → validate → load.
120
- // The loader registers a Bun resolver plugin that maps `atomic/*` and
121
- // atomic's installed deps onto the running CLI's own module graph, so
122
- // workflow files don't need their own `package.json` / `node_modules`.
120
+ // External workflows must have `@bastani/atomic` installed as a dependency.
123
121
  const result = await WorkflowLoader.loadWorkflow(discovered, {
124
122
  warn(warnings) {
125
123
  for (const w of warnings) {
@@ -185,16 +183,18 @@ const AGENT_DISPLAY_NAMES: Record<AgentType, string> = {
185
183
  copilot: "Copilot CLI",
186
184
  };
187
185
  /** Local first — project-scoped workflows are the most immediately relevant. */
188
- const SOURCE_ORDER: readonly DiscoveredWorkflow["source"][] = ["local", "global"];
186
+ const SOURCE_ORDER: readonly DiscoveredWorkflow["source"][] = ["local", "global", "builtin"];
189
187
  /** Friendly directory labels shown inline with each section heading. */
190
188
  const SOURCE_DIRS: Record<DiscoveredWorkflow["source"], string> = {
191
189
  local: ".atomic/workflows",
192
190
  global: "~/.atomic/workflows",
191
+ builtin: "built-in",
193
192
  };
194
193
  /** Section heading colour per source — preserves the source-type semantic. */
195
194
  const SOURCE_COLORS: Record<DiscoveredWorkflow["source"], PaletteKey> = {
196
195
  local: "success",
197
196
  global: "mauve",
197
+ builtin: "accent",
198
198
  };
199
199
 
200
200
  /**
@@ -5,8 +5,8 @@
5
5
  * change propagates everywhere.
6
6
  */
7
7
 
8
- import { AGENTS } from "@/sdk/workflows.ts";
9
- import type { AgentType } from "@/sdk/workflows.ts";
8
+ import { AGENTS } from "@/sdk/workflows/index.ts";
9
+ import type { AgentType } from "@/sdk/workflows/index.ts";
10
10
 
11
11
  export {
12
12
  SDK_PACKAGE_NAME,
@@ -127,9 +127,7 @@ export function SessionGraphPanel() {
127
127
  setAttachMsg(`\u2192 ${n.name}`);
128
128
  attachTimerRef.current = setTimeout(() => setAttachMsg(""), ATTACH_MSG_DISPLAY_MS);
129
129
 
130
- try {
131
- tmuxRun(["select-window", "-t", `${tmuxSession}:${n.name}`]);
132
- } catch {}
130
+ tmuxRun(["switch-client", "-t", `${tmuxSession}:${n.name}`]);
133
131
  },
134
132
  [layout.map, tmuxSession, store.sessions],
135
133
  );
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { join } from "path";
12
12
  import { readdir, writeFile } from "fs/promises";
13
+ import { existsSync, readdirSync } from "fs";
13
14
  import { homedir } from "os";
14
15
  import ignore from "ignore";
15
16
  import type { AgentType } from "../types.ts";
@@ -18,7 +19,7 @@ export interface DiscoveredWorkflow {
18
19
  name: string;
19
20
  agent: AgentType;
20
21
  path: string;
21
- source: "local" | "global";
22
+ source: "local" | "global" | "builtin";
22
23
  }
23
24
 
24
25
  function getLocalWorkflowsDir(projectRoot: string): string {
@@ -131,8 +132,50 @@ async function discoverFromBaseDir(
131
132
  }
132
133
 
133
134
  /**
134
- * Discover all available workflows from local and global directories.
135
- * Optionally filter by agent. Local workflows take precedence over global.
135
+ * Absolute path to the `builtin/` directory inside the SDK source tree.
136
+ * Computed from this file's URL so it always resolves correctly regardless
137
+ * of how the package was installed (dev checkout, global, bunx, etc.).
138
+ */
139
+ const BUILTIN_WORKFLOWS_DIR = join(
140
+ Bun.fileURLToPath(new URL("../workflows/builtin", import.meta.url)),
141
+ );
142
+
143
+ /**
144
+ * Discover built-in workflows shipped as SDK modules.
145
+ *
146
+ * Scans `src/sdk/workflows/builtin/<name>/<agent>/index.ts` for known
147
+ * workflow directories. Returns entries with `source: "builtin"`.
148
+ */
149
+ function discoverBuiltinWorkflows(
150
+ agentFilter?: AgentType,
151
+ ): DiscoveredWorkflow[] {
152
+ const results: DiscoveredWorkflow[] = [];
153
+ const agents = agentFilter ? [agentFilter] : AGENTS;
154
+
155
+ let workflowNames: string[];
156
+ try {
157
+ workflowNames = readdirSync(BUILTIN_WORKFLOWS_DIR, { withFileTypes: true })
158
+ .filter((d) => d.isDirectory())
159
+ .map((d) => d.name);
160
+ } catch {
161
+ return results;
162
+ }
163
+
164
+ for (const name of workflowNames) {
165
+ for (const agent of agents) {
166
+ const indexPath = join(BUILTIN_WORKFLOWS_DIR, name, agent, "index.ts");
167
+ if (existsSync(indexPath)) {
168
+ results.push({ name, agent, path: indexPath, source: "builtin" });
169
+ }
170
+ }
171
+ }
172
+
173
+ return results;
174
+ }
175
+
176
+ /**
177
+ * Discover all available workflows from built-in, global, and local sources.
178
+ * Optionally filter by agent. Precedence: local > global > builtin.
136
179
  */
137
180
  export async function discoverWorkflows(
138
181
  projectRoot: string = process.cwd(),
@@ -141,12 +184,17 @@ export async function discoverWorkflows(
141
184
  const localDir = getLocalWorkflowsDir(projectRoot);
142
185
  const globalDir = getGlobalWorkflowsDir();
143
186
 
187
+ const builtinResults = discoverBuiltinWorkflows(agentFilter);
144
188
  const [globalResults, localResults] = await Promise.all([
145
189
  discoverFromBaseDir(globalDir, "global", agentFilter),
146
190
  discoverFromBaseDir(localDir, "local", agentFilter),
147
191
  ]);
148
192
 
193
+ // Merge with precedence: builtin (lowest) → global → local (highest)
149
194
  const byKey = new Map<string, DiscoveredWorkflow>();
195
+ for (const wf of builtinResults) {
196
+ byKey.set(`${wf.agent}/${wf.name}`, wf);
197
+ }
150
198
  for (const wf of globalResults) {
151
199
  byKey.set(`${wf.agent}/${wf.name}`, wf);
152
200
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Orchestrator entry point — invoked inside a tmux pane by the launcher script.
3
+ *
4
+ * Separated from executor.ts to avoid the dual-module-identity problem:
5
+ * Bun evaluates a file twice when it is both the entry point (`bun run`)
6
+ * and reached through package.json `exports` self-referencing. Keeping
7
+ * the side-effectful `--run` guard here ensures executor.ts stays a pure
8
+ * library module that can be safely re-exported from the SDK barrel.
9
+ */
10
+
11
+ import { runOrchestrator } from "./executor.ts";
12
+
13
+ runOrchestrator().catch((err) => {
14
+ console.error("Fatal:", err);
15
+ process.exitCode = 1;
16
+ });