@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
@@ -1,103 +0,0 @@
1
- /**
2
- * Hello-parallel workflow for Copilot — parallel session example.
3
- *
4
- * Session 1 (sequential): Ask the agent to describe the project.
5
- * Sessions 2+3 (parallel): Two agents summarize session 1 concurrently.
6
- * Session 4 (sequential): Merge both summaries into a final output.
7
- *
8
- * Run: atomic workflow -n hello-parallel -a copilot "describe this project"
9
- */
10
-
11
- import { defineWorkflow } from "@bastani/atomic/workflows";
12
-
13
- /**
14
- * `CopilotSession.sendAndWait` defaults to a 60s timeout and THROWS on
15
- * expiry, which crashes the workflow mid-stage. Override with a generous
16
- * 30-minute budget so legitimate long-running agent work completes.
17
- */
18
- const SEND_TIMEOUT_MS = 30 * 60 * 1000;
19
-
20
- export default defineWorkflow<"copilot">({
21
- name: "hello-parallel",
22
- description: "Parallel Copilot demo: describe → [summarize-a, summarize-b] → merge",
23
- })
24
- .run(async (ctx) => {
25
- // Sequential: describe
26
- const describe = await ctx.stage(
27
- { name: "describe", description: "Ask the agent to describe the project" },
28
- {},
29
- {},
30
- async (s) => {
31
- await s.session.sendAndWait({ prompt: s.userPrompt }, SEND_TIMEOUT_MS);
32
-
33
- s.save(await s.session.getMessages());
34
- },
35
- );
36
-
37
- // Parallel: summarize-a + summarize-b
38
- const [summarizeA, summarizeB] = await Promise.all([
39
- ctx.stage(
40
- { name: "summarize-a", description: "Summarize the description as bullet points" },
41
- {},
42
- {},
43
- async (s) => {
44
- const research = await s.transcript(describe);
45
-
46
- await s.session.sendAndWait(
47
- {
48
- prompt: `Summarize the following in 2-3 bullet points:\n\n${research.content}`,
49
- },
50
- SEND_TIMEOUT_MS,
51
- );
52
-
53
- s.save(await s.session.getMessages());
54
- },
55
- ),
56
- ctx.stage(
57
- { name: "summarize-b", description: "Summarize the description as a one-liner" },
58
- {},
59
- {},
60
- async (s) => {
61
- const research = await s.transcript(describe);
62
-
63
- await s.session.sendAndWait(
64
- {
65
- prompt: `Summarize the following in a single sentence:\n\n${research.content}`,
66
- },
67
- SEND_TIMEOUT_MS,
68
- );
69
-
70
- s.save(await s.session.getMessages());
71
- },
72
- ),
73
- ]);
74
-
75
- // Sequential: merge
76
- await ctx.stage(
77
- { name: "merge", description: "Merge both summaries into a final output" },
78
- {},
79
- {},
80
- async (s) => {
81
- const bullets = await s.transcript(summarizeA);
82
- const oneliner = await s.transcript(summarizeB);
83
-
84
- await s.session.sendAndWait(
85
- {
86
- prompt: [
87
- "Combine the following two summaries into one concise paragraph:",
88
- "",
89
- "## Bullet points",
90
- bullets.content,
91
- "",
92
- "## One-liner",
93
- oneliner.content,
94
- ].join("\n"),
95
- },
96
- SEND_TIMEOUT_MS,
97
- );
98
-
99
- s.save(await s.session.getMessages());
100
- },
101
- );
102
- })
103
- .compile();
@@ -1,105 +0,0 @@
1
- /**
2
- * Hello-parallel workflow for OpenCode — parallel session example.
3
- *
4
- * Session 1 (sequential): Ask the agent to describe the project.
5
- * Sessions 2+3 (parallel): Two agents summarize session 1 concurrently.
6
- * Session 4 (sequential): Merge both summaries into a final output.
7
- *
8
- * Run: atomic workflow -n hello-parallel -a opencode "describe this project"
9
- */
10
-
11
- import { defineWorkflow } from "@bastani/atomic/workflows";
12
-
13
- export default defineWorkflow<"opencode">({
14
- name: "hello-parallel",
15
- description: "Parallel OpenCode demo: describe → [summarize-a, summarize-b] → merge",
16
- })
17
- .run(async (ctx) => {
18
- const describe = await ctx.stage(
19
- { name: "describe", description: "Ask the agent to describe the project" },
20
- {},
21
- { title: "describe" },
22
- async (s) => {
23
- const result = await s.client.session.prompt({
24
- sessionID: s.session.id,
25
- parts: [{ type: "text", text: s.userPrompt }],
26
- });
27
-
28
- s.save(result.data!);
29
- },
30
- );
31
-
32
- const [summarizeA, summarizeB] = await Promise.all([
33
- ctx.stage(
34
- { name: "summarize-a", description: "Summarize the description as bullet points" },
35
- {},
36
- { title: "summarize-a" },
37
- async (s) => {
38
- const research = await s.transcript(describe);
39
-
40
- const result = await s.client.session.prompt({
41
- sessionID: s.session.id,
42
- parts: [
43
- {
44
- type: "text",
45
- text: `Summarize the following in 2-3 bullet points:\n\n${research.content}`,
46
- },
47
- ],
48
- });
49
-
50
- s.save(result.data!);
51
- },
52
- ),
53
- ctx.stage(
54
- { name: "summarize-b", description: "Summarize the description as a one-liner" },
55
- {},
56
- { title: "summarize-b" },
57
- async (s) => {
58
- const research = await s.transcript(describe);
59
-
60
- const result = await s.client.session.prompt({
61
- sessionID: s.session.id,
62
- parts: [
63
- {
64
- type: "text",
65
- text: `Summarize the following in a single sentence:\n\n${research.content}`,
66
- },
67
- ],
68
- });
69
-
70
- s.save(result.data!);
71
- },
72
- ),
73
- ]);
74
-
75
- await ctx.stage(
76
- { name: "merge", description: "Merge both summaries into a final output" },
77
- {},
78
- { title: "merge" },
79
- async (s) => {
80
- const bullets = await s.transcript(summarizeA);
81
- const oneliner = await s.transcript(summarizeB);
82
-
83
- const result = await s.client.session.prompt({
84
- sessionID: s.session.id,
85
- parts: [
86
- {
87
- type: "text",
88
- text: [
89
- "Combine the following two summaries into one concise paragraph:",
90
- "",
91
- "## Bullet points",
92
- bullets.content,
93
- "",
94
- "## One-liner",
95
- oneliner.content,
96
- ].join("\n"),
97
- },
98
- ],
99
- });
100
-
101
- s.save(result.data!);
102
- },
103
- );
104
- })
105
- .compile();
@@ -1,22 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "allowImportingTsExtensions": true,
7
- "noEmit": true,
8
- "verbatimModuleSyntax": true,
9
- "strict": true,
10
- "skipLibCheck": true,
11
- "types": ["bun"],
12
- "paths": {
13
- "@bastani/atomic/workflows": ["../../src/sdk/workflows.ts"]
14
- }
15
- },
16
- "include": [
17
- "**/claude/**/*.ts",
18
- "**/copilot/**/*.ts",
19
- "**/opencode/**/*.ts",
20
- "**/helpers/**/*.ts"
21
- ]
22
- }
@@ -1,240 +0,0 @@
1
- /**
2
- * Sync bundled Atomic workflow templates from the installed package into
3
- * the user's global `~/.atomic/workflows/` directory.
4
- *
5
- * Each bundled workflow directory (`hello/`, `hello-parallel/`, `ralph/`,
6
- * etc.) is a full overwrite of its destination — `rm -rf` followed by a
7
- * fresh copy — so files removed upstream don't linger after an upgrade.
8
- * User-created workflows whose names don't collide with bundled names are
9
- * left untouched (we only iterate the bundled source, never the user's
10
- * destination).
11
- *
12
- * Root-level files (`tsconfig.json`, etc.) are also overwritten on each
13
- * sync.
14
- */
15
-
16
- import { join, sep } from "path";
17
- import { lstat, readdir, rm, symlink, unlink } from "fs/promises";
18
- import { homedir } from "os";
19
- import {
20
- copyDir,
21
- copyFile,
22
- ensureDir,
23
- pathExists,
24
- } from "@/services/system/copy.ts";
25
- import { assertPathWithinRoot } from "@/lib/path-root-guard.ts";
26
-
27
- /**
28
- * Reject any entry name that could redirect a write outside destRoot.
29
- * `readdir` should never return `.`, `..`, or names with separators, but
30
- * this is a cheap belt-and-braces check in case the installed package has
31
- * been tampered with or sits on an exotic filesystem.
32
- */
33
- function isSafeEntryName(name: string): boolean {
34
- if (name === "" || name === "." || name === "..") return false;
35
- if (name.includes("/") || name.includes("\\")) return false;
36
- if (sep !== "/" && name.includes(sep)) return false;
37
- return true;
38
- }
39
-
40
- /**
41
- * Locate the package root by walking up from this module. Both in installed
42
- * (`<pkg>/src/services/system/workflows.ts`) and dev checkout layouts the
43
- * package root is three directories up.
44
- */
45
- function packageRoot(): string {
46
- return join(import.meta.dir, "..", "..", "..");
47
- }
48
-
49
- /**
50
- * Safely remove a symlink or junction before re-creating it.
51
- *
52
- * On Windows, Bun's `rm({ recursive: true })` follows NTFS junctions and
53
- * can delete the **target directory's contents** (oven-sh/bun#27233).
54
- * `unlink` is safe: it opens with `FILE_FLAG_OPEN_REPARSE_POINT` and
55
- * removes only the link entry, never following it.
56
- *
57
- * Falls back to `rm` only when the path is a real directory (not a link),
58
- * which can happen if a previous version created the path as a plain copy
59
- * instead of a symlink.
60
- */
61
- async function removeLinkOrDir(path: string): Promise<void> {
62
- try {
63
- const stats = await lstat(path);
64
- if (stats.isSymbolicLink()) {
65
- await unlink(path);
66
- } else if (stats.isDirectory()) {
67
- await rm(path, { recursive: true, force: true });
68
- } else {
69
- await unlink(path);
70
- }
71
- } catch (error: unknown) {
72
- // ENOENT — path doesn't exist; nothing to remove.
73
- if (
74
- error instanceof Error &&
75
- "code" in error &&
76
- (error as NodeJS.ErrnoException).code === "ENOENT"
77
- ) {
78
- return;
79
- }
80
- throw error;
81
- }
82
- }
83
-
84
- /** Honors ATOMIC_SETTINGS_HOME so tests can point at a temp dir. */
85
- function homeRoot(): string {
86
- return process.env.ATOMIC_SETTINGS_HOME ?? homedir();
87
- }
88
-
89
- /**
90
- * Sync bundled workflow templates to `~/.atomic/workflows/`. Returns the
91
- * number of bundled workflows installed (for logging). Best-effort on
92
- * individual entries — readdir failures throw, per-entry copy failures
93
- * propagate up to the caller (auto-sync's runStep).
94
- */
95
- export async function installGlobalWorkflows(): Promise<void> {
96
- const srcRoot = join(packageRoot(), ".atomic", "workflows");
97
- const destRoot = join(homeRoot(), ".atomic", "workflows");
98
-
99
- if (!(await pathExists(srcRoot))) {
100
- // Treat a missing bundled source as a non-fatal skip: dev checkouts
101
- // and partial installs legitimately hit this path. Surfaced via a
102
- // thrown error so the spinner UI marks the step red with context.
103
- throw new Error(`bundled workflows missing at ${srcRoot} — skipping ${destRoot}`);
104
- }
105
-
106
- await ensureDir(destRoot);
107
-
108
- // Safety invariant: we enumerate the BUNDLED source, never the user's
109
- // destination. This guarantees that `rm(dest)` can only ever target a
110
- // path whose basename exists in the bundled workflows — user-created
111
- // workflows with different names are structurally invisible to this loop.
112
- const entries = await readdir(srcRoot, { withFileTypes: true });
113
-
114
- for (const entry of entries) {
115
- if (!isSafeEntryName(entry.name)) {
116
- console.warn(
117
- ` skipping unsafe bundled workflow entry name: ${JSON.stringify(entry.name)}`,
118
- );
119
- continue;
120
- }
121
-
122
- const src = join(srcRoot, entry.name);
123
- const dest = join(destRoot, entry.name);
124
-
125
- // Belt-and-braces: confirm the computed destination actually lives
126
- // inside destRoot. Throws if something conspired to produce an escape.
127
- assertPathWithinRoot(destRoot, dest, "Workflow destination");
128
-
129
- if (entry.isFile()) {
130
- // Root files (tsconfig.json, etc.) — overwrite in place.
131
- await copyFile(src, dest);
132
- } else if (entry.isDirectory()) {
133
- // Bundled workflow — full overwrite of the destination directory so
134
- // files removed upstream don't linger across upgrades. User-created
135
- // workflows under names that don't collide are untouched.
136
- await rm(dest, { recursive: true, force: true });
137
- await copyDir(src, dest);
138
- }
139
- }
140
-
141
- // ── Type-resolution setup for workflow authors ─────────────────────
142
- //
143
- // The bundled tsconfig.json uses relative `paths` that only resolve
144
- // correctly inside the package's own directory tree. Once the files
145
- // are copied to `~/.atomic/workflows/`, those relative paths break.
146
- //
147
- // Strategy: symlink `node_modules/@bastani/atomic` in the destination
148
- // back to the running package root. TypeScript's standard module
149
- // resolution then finds `@bastani/atomic/workflows` (and its
150
- // transitive deps) automatically — no `paths` override needed.
151
- //
152
- // If symlink creation fails (permissions, unsupported FS), we fall
153
- // back to a tsconfig with an absolute `paths` entry pointing at the
154
- // package's SDK source. Either way the workflow author gets types
155
- // with zero manual configuration.
156
- await setupWorkflowTypes(destRoot);
157
- }
158
-
159
- /**
160
- * Wire up TypeScript type resolution for a global workflows directory.
161
- *
162
- * Creates a `node_modules/@bastani/atomic` symlink → the installed
163
- * package root and generates a tsconfig.json that lets standard module
164
- * resolution do the work. Falls back to absolute `paths` in the
165
- * tsconfig if symlinking isn't possible.
166
- */
167
- export async function setupWorkflowTypes(destRoot: string): Promise<void> {
168
- const pkgRoot = packageRoot();
169
- let usedSymlink = false;
170
-
171
- // 1. Symlink the package itself
172
- try {
173
- const scopeDir = join(destRoot, "node_modules", "@bastani");
174
- await ensureDir(scopeDir);
175
-
176
- const link = join(scopeDir, "atomic");
177
- await removeLinkOrDir(link);
178
-
179
- // Junctions on Windows need no elevated privileges.
180
- const type = process.platform === "win32" ? "junction" : "dir";
181
- await symlink(pkgRoot, link, type);
182
- usedSymlink = true;
183
- } catch {
184
- // Swallow — falls back to paths-based tsconfig below.
185
- }
186
-
187
- // 2. Symlink @types/bun so `Bun.*` APIs have types in workflows
188
- try {
189
- const bunTypes = join(pkgRoot, "node_modules", "@types", "bun");
190
- if (await pathExists(bunTypes)) {
191
- const typesDir = join(destRoot, "node_modules", "@types");
192
- await ensureDir(typesDir);
193
-
194
- const link = join(typesDir, "bun");
195
- await removeLinkOrDir(link);
196
-
197
- const type = process.platform === "win32" ? "junction" : "dir";
198
- await symlink(bunTypes, link, type);
199
- }
200
- } catch {
201
- // Best effort — Bun APIs in workflows lack types but runtime is fine.
202
- }
203
-
204
- // 3. Generate a clean tsconfig for the destination
205
- const compilerOptions: Record<string, unknown> = {
206
- target: "ESNext",
207
- module: "ESNext",
208
- moduleResolution: "bundler",
209
- allowImportingTsExtensions: true,
210
- noEmit: true,
211
- verbatimModuleSyntax: true,
212
- strict: true,
213
- skipLibCheck: true,
214
- types: ["bun"],
215
- };
216
-
217
- if (!usedSymlink) {
218
- // Fallback: absolute paths so TypeScript can still resolve the SDK
219
- // source from the installed package location.
220
- compilerOptions.paths = {
221
- "@bastani/atomic": [join(pkgRoot, "src", "sdk", "index.ts")],
222
- "@bastani/atomic/workflows": [join(pkgRoot, "src", "sdk", "workflows.ts")],
223
- };
224
- }
225
-
226
- const tsconfig = { compilerOptions, include: GLOBAL_TSCONFIG_INCLUDE };
227
-
228
- await Bun.write(
229
- join(destRoot, "tsconfig.json"),
230
- JSON.stringify(tsconfig, null, 2) + "\n",
231
- );
232
- }
233
-
234
- /** Include globs shared by every generated global workflows tsconfig. */
235
- const GLOBAL_TSCONFIG_INCLUDE = [
236
- "**/claude/**/*.ts",
237
- "**/copilot/**/*.ts",
238
- "**/opencode/**/*.ts",
239
- "**/helpers/**/*.ts",
240
- ];