@bastani/atomic 0.6.4-0 → 0.6.5-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/create-spec/SKILL.md +6 -3
- package/.agents/skills/tdd/SKILL.md +107 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/workflow-creator/SKILL.md +550 -0
- package/.agents/skills/workflow-creator/references/agent-sessions.md +891 -0
- package/.agents/skills/workflow-creator/references/agent-setup-recipe.md +266 -0
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +201 -0
- package/.agents/skills/workflow-creator/references/control-flow.md +470 -0
- package/.agents/skills/workflow-creator/references/failure-modes.md +1014 -0
- package/.agents/skills/workflow-creator/references/getting-started.md +392 -0
- package/.agents/skills/workflow-creator/references/registry-and-validation.md +141 -0
- package/.agents/skills/workflow-creator/references/running-workflows.md +418 -0
- package/.agents/skills/workflow-creator/references/session-config.md +384 -0
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +356 -0
- package/.agents/skills/workflow-creator/references/user-input.md +234 -0
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +392 -0
- package/.claude/agents/debugger.md +2 -2
- package/.claude/agents/reviewer.md +1 -1
- package/.claude/agents/worker.md +2 -2
- package/.github/agents/debugger.md +1 -1
- package/.github/agents/worker.md +1 -1
- package/.mcp.json +5 -1
- package/.opencode/agents/debugger.md +1 -1
- package/.opencode/agents/worker.md +1 -1
- package/README.md +236 -201
- package/dist/sdk/define-workflow.d.ts +11 -6
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/errors.d.ts +10 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +21 -9
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/primitives/inputs.d.ts +36 -0
- package/dist/sdk/primitives/inputs.d.ts.map +1 -0
- package/dist/sdk/primitives/metadata.d.ts +40 -0
- package/dist/sdk/primitives/metadata.d.ts.map +1 -0
- package/dist/sdk/primitives/run.d.ts +57 -0
- package/dist/sdk/primitives/run.d.ts.map +1 -0
- package/dist/sdk/primitives/sessions.d.ts +128 -0
- package/dist/sdk/primitives/sessions.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +24 -56
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/orchestrator-entry.d.ts +26 -0
- package/dist/sdk/runtime/orchestrator-entry.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +20 -0
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +26 -86
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/index.d.ts +20 -12
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/dist/services/config/additional-instructions.d.ts +1 -1
- package/dist/services/config/additional-instructions.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/cli.ts +39 -56
- package/src/commands/builtin-registry.ts +37 -0
- package/src/commands/cli/chat/index.ts +1 -3
- package/src/{sdk → commands/cli}/management-commands.ts +15 -55
- package/src/commands/cli/session.ts +1 -1
- package/src/commands/cli/workflow-command.test.ts +250 -16
- package/src/commands/cli/workflow-inputs.test.ts +1 -0
- package/src/commands/cli/workflow-inputs.ts +13 -3
- package/src/commands/cli/workflow-list.test.ts +1 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow-status.ts +1 -1
- package/src/commands/cli/workflow.ts +191 -11
- package/src/sdk/define-workflow.test.ts +47 -16
- package/src/sdk/define-workflow.ts +24 -6
- package/src/sdk/errors.test.ts +11 -0
- package/src/sdk/errors.ts +13 -0
- package/src/sdk/index.test.ts +92 -0
- package/src/sdk/index.ts +71 -15
- package/src/sdk/primitives/inputs.ts +48 -0
- package/src/sdk/primitives/metadata.ts +63 -0
- package/src/sdk/primitives/run.ts +81 -0
- package/src/sdk/primitives/sessions.test.ts +594 -0
- package/src/sdk/primitives/sessions.ts +328 -0
- package/src/sdk/runtime/executor.ts +36 -115
- package/src/sdk/runtime/orchestrator-entry.ts +110 -0
- package/src/sdk/runtime/tmux.ts +33 -0
- package/src/sdk/types.ts +26 -91
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -0
- package/src/sdk/workflows/index.ts +68 -51
- package/src/services/config/additional-instructions.ts +1 -1
- package/.agents/skills/test-driven-development/SKILL.md +0 -371
- package/.agents/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/dist/commands/cli/session.d.ts +0 -67
- package/dist/commands/cli/session.d.ts.map +0 -1
- package/dist/commands/cli/workflow-status.d.ts +0 -63
- package/dist/commands/cli/workflow-status.d.ts.map +0 -1
- package/dist/sdk/commander.d.ts +0 -74
- package/dist/sdk/commander.d.ts.map +0 -1
- package/dist/sdk/management-commands.d.ts +0 -42
- package/dist/sdk/management-commands.d.ts.map +0 -1
- package/dist/sdk/workflow-cli.d.ts +0 -103
- package/dist/sdk/workflow-cli.d.ts.map +0 -1
- package/dist/sdk/workflows/builtin-registry.d.ts +0 -113
- package/dist/sdk/workflows/builtin-registry.d.ts.map +0 -1
- package/src/sdk/commander.ts +0 -161
- package/src/sdk/workflow-cli.ts +0 -409
- package/src/sdk/workflows/builtin-registry.ts +0 -23
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Workflow Authors: Getting Started
|
|
2
|
+
|
|
3
|
+
This guide covers the basics of creating workflows with the `defineWorkflow().run().compile()` API and wiring them into a composition root.
|
|
4
|
+
|
|
5
|
+
## Composition root
|
|
6
|
+
|
|
7
|
+
A workflow's composition root is the TypeScript file a user runs via `bun`. The SDK exposes pure primitives — there's no opinionated wrapper. Compose them into whatever CLI library you prefer (Commander, citty, yargs, or none at all) and call `runWorkflow({ workflow, inputs })` from the action.
|
|
8
|
+
|
|
9
|
+
### Single workflow
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// src/claude-worker.ts
|
|
13
|
+
import { Command } from "@commander-js/extra-typings";
|
|
14
|
+
import { getInputSchema, runWorkflow } from "@bastani/atomic/workflows";
|
|
15
|
+
import workflow from "./workflows/deploy/claude.ts";
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
for (const input of getInputSchema(workflow)) {
|
|
19
|
+
program.option(`--${input.name} <value>`, input.description ?? "");
|
|
20
|
+
}
|
|
21
|
+
program.action(async (rawOpts) => {
|
|
22
|
+
await runWorkflow({ workflow, inputs: rawOpts as Record<string, string> });
|
|
23
|
+
});
|
|
24
|
+
await program.parseAsync();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run it:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun run src/claude-worker.ts --prompt "your task"
|
|
31
|
+
bun run src/claude-worker.ts --field=value
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Multiple workflows
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// src/cli.ts
|
|
38
|
+
import { Command } from "@commander-js/extra-typings";
|
|
39
|
+
import {
|
|
40
|
+
createRegistry,
|
|
41
|
+
getInputSchema,
|
|
42
|
+
getName,
|
|
43
|
+
listWorkflows,
|
|
44
|
+
runWorkflow,
|
|
45
|
+
} from "@bastani/atomic/workflows";
|
|
46
|
+
import claudeFlow from "./workflows/my-flow/claude.ts";
|
|
47
|
+
import copilotFlow from "./workflows/my-flow/copilot.ts";
|
|
48
|
+
|
|
49
|
+
const registry = createRegistry().register(claudeFlow).register(copilotFlow);
|
|
50
|
+
const program = new Command();
|
|
51
|
+
|
|
52
|
+
for (const wf of listWorkflows(registry)) {
|
|
53
|
+
const sub = program.command(getName(wf)).description(wf.description);
|
|
54
|
+
for (const input of getInputSchema(wf)) {
|
|
55
|
+
sub.option(`--${input.name} <value>`, input.description ?? "");
|
|
56
|
+
}
|
|
57
|
+
sub.action(async (rawOpts) => {
|
|
58
|
+
await runWorkflow({ workflow: wf, inputs: rawOpts as Record<string, string> });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
await program.parseAsync();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Programmatic invocation (no CLI)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { runWorkflow } from "@bastani/atomic/workflows";
|
|
68
|
+
import workflow from "./workflows/deploy/claude.ts";
|
|
69
|
+
|
|
70
|
+
const { id, tmuxSessionName } = await runWorkflow({
|
|
71
|
+
workflow,
|
|
72
|
+
inputs: { prompt: "task" },
|
|
73
|
+
detach: true,
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Detach and monitor
|
|
78
|
+
|
|
79
|
+
`runWorkflow({ ..., detach: true })` returns immediately after the tmux session is created. Combine with `getSessionStatus(tmuxSessionName)`, `attachSession(id)`, and `stopSession(id)` from `@bastani/atomic/workflows` to build your own monitoring loop, or use the global `atomic session …` / `atomic workflow status` commands.
|
|
80
|
+
|
|
81
|
+
### Interactive picker
|
|
82
|
+
|
|
83
|
+
The same picker `atomic workflow -a claude` opens is exposed as a component:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { WorkflowPickerPanel } from "@bastani/atomic/workflows/components";
|
|
87
|
+
|
|
88
|
+
const panel = await WorkflowPickerPanel.create({ agent: "claude", registry });
|
|
89
|
+
const result = await panel.waitForSelection();
|
|
90
|
+
panel.destroy();
|
|
91
|
+
if (result) {
|
|
92
|
+
await runWorkflow({ workflow: result.workflow, inputs: result.inputs });
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quick-start example
|
|
97
|
+
|
|
98
|
+
Use `defineWorkflow({...}).for("agent").run(callback).compile()` to define your workflow. Pass the agent as a runtime string argument to `.for()` — this narrows the context types for everything downstream. Inside the `.run()` callback, use `ctx.stage()` to spawn agent sessions dynamically. Each session gets its own tmux window and graph node. Use native TypeScript control flow (`for`, `if`, `Promise.all()`) for orchestration.
|
|
99
|
+
|
|
100
|
+
The runtime manages the full session lifecycle automatically — it creates the client, creates the session, runs your callback, then cleans up. You never need to manually disconnect or stop anything.
|
|
101
|
+
|
|
102
|
+
### Claude
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// src/workflows/my-workflow/claude.ts
|
|
106
|
+
import { defineWorkflow, extractAssistantText } from "@bastani/atomic/workflows";
|
|
107
|
+
|
|
108
|
+
export default defineWorkflow({
|
|
109
|
+
name: "my-workflow",
|
|
110
|
+
source: import.meta.path,
|
|
111
|
+
description: "A two-session pipeline",
|
|
112
|
+
inputs: [
|
|
113
|
+
{ name: "prompt", type: "text", required: true, description: "task to perform" },
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
.for("claude")
|
|
117
|
+
.run(async (ctx) => {
|
|
118
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
119
|
+
|
|
120
|
+
const describe = await ctx.stage(
|
|
121
|
+
{ name: "describe", description: "Ask Claude to describe the project" },
|
|
122
|
+
{},
|
|
123
|
+
{},
|
|
124
|
+
async (s) => {
|
|
125
|
+
await s.session.query(prompt);
|
|
126
|
+
s.save(s.sessionId);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
await ctx.stage(
|
|
131
|
+
{ name: "summarize", description: "Summarize the previous session's output" },
|
|
132
|
+
{},
|
|
133
|
+
{},
|
|
134
|
+
async (s) => {
|
|
135
|
+
const research = await s.transcript(describe);
|
|
136
|
+
await s.session.query(
|
|
137
|
+
`Read ${research.path} and summarize it in 2-3 bullet points.`,
|
|
138
|
+
);
|
|
139
|
+
s.save(s.sessionId);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
})
|
|
143
|
+
.compile();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Copilot
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// src/workflows/my-workflow/copilot.ts
|
|
150
|
+
import { defineWorkflow } from "@bastani/atomic/workflows";
|
|
151
|
+
|
|
152
|
+
export default defineWorkflow({
|
|
153
|
+
name: "my-workflow",
|
|
154
|
+
source: import.meta.path,
|
|
155
|
+
description: "A two-session pipeline",
|
|
156
|
+
inputs: [
|
|
157
|
+
{ name: "prompt", type: "text", required: true, description: "task to perform" },
|
|
158
|
+
],
|
|
159
|
+
})
|
|
160
|
+
.for("copilot")
|
|
161
|
+
.run(async (ctx) => {
|
|
162
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
163
|
+
|
|
164
|
+
const describe = await ctx.stage(
|
|
165
|
+
{ name: "describe", description: "Ask the agent to describe the project" },
|
|
166
|
+
{},
|
|
167
|
+
{},
|
|
168
|
+
async (s) => {
|
|
169
|
+
await s.session.send({ prompt });
|
|
170
|
+
s.save(await s.session.getMessages());
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await ctx.stage(
|
|
175
|
+
{ name: "summarize", description: "Summarize the previous session's output" },
|
|
176
|
+
{},
|
|
177
|
+
{},
|
|
178
|
+
async (s) => {
|
|
179
|
+
const research = await s.transcript(describe);
|
|
180
|
+
await s.session.send({
|
|
181
|
+
prompt: `Summarize the following in 2-3 bullet points:\n\n${research.content}`,
|
|
182
|
+
});
|
|
183
|
+
s.save(await s.session.getMessages());
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
})
|
|
187
|
+
.compile();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### OpenCode
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// src/workflows/my-workflow/opencode.ts
|
|
194
|
+
import { defineWorkflow } from "@bastani/atomic/workflows";
|
|
195
|
+
|
|
196
|
+
export default defineWorkflow({
|
|
197
|
+
name: "my-workflow",
|
|
198
|
+
source: import.meta.path,
|
|
199
|
+
description: "A two-session pipeline",
|
|
200
|
+
inputs: [
|
|
201
|
+
{ name: "prompt", type: "text", required: true, description: "task to perform" },
|
|
202
|
+
],
|
|
203
|
+
})
|
|
204
|
+
.for("opencode")
|
|
205
|
+
.run(async (ctx) => {
|
|
206
|
+
const prompt = ctx.inputs.prompt ?? "";
|
|
207
|
+
|
|
208
|
+
const describe = await ctx.stage(
|
|
209
|
+
{ name: "describe", description: "Ask the agent to describe the project" },
|
|
210
|
+
{},
|
|
211
|
+
{ title: "describe" },
|
|
212
|
+
async (s) => {
|
|
213
|
+
const result = await s.client.session.prompt({
|
|
214
|
+
sessionID: s.session.id,
|
|
215
|
+
parts: [{ type: "text", text: prompt }],
|
|
216
|
+
});
|
|
217
|
+
s.save(result.data!);
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
await ctx.stage(
|
|
222
|
+
{ name: "summarize", description: "Summarize the previous session's output" },
|
|
223
|
+
{},
|
|
224
|
+
{ title: "summarize" },
|
|
225
|
+
async (s) => {
|
|
226
|
+
const research = await s.transcript(describe);
|
|
227
|
+
const result = await s.client.session.prompt({
|
|
228
|
+
sessionID: s.session.id,
|
|
229
|
+
parts: [{ type: "text", text: `Summarize the following in 2-3 bullet points:\n\n${research.content}` }],
|
|
230
|
+
});
|
|
231
|
+
s.save(result.data!);
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
})
|
|
235
|
+
.compile();
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Reading top-to-bottom: `describe → summarize`. Each session spawns a graph node and tmux window.
|
|
239
|
+
|
|
240
|
+
## Native TypeScript control flow
|
|
241
|
+
|
|
242
|
+
Sessions are spawned dynamically, so you can use loops, conditionals, and `Promise.all()`:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// Parallel sessions
|
|
246
|
+
const [a, b] = await Promise.all([
|
|
247
|
+
ctx.stage({ name: "task-a" }, {}, {}, async (s) => { /* ... */ }),
|
|
248
|
+
ctx.stage({ name: "task-b" }, {}, {}, async (s) => { /* ... */ }),
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
// Loop with dynamic sessions
|
|
252
|
+
for (let i = 1; i <= maxIterations; i++) {
|
|
253
|
+
const result = await ctx.stage({ name: `step-${i}` }, {}, {}, async (s) => {
|
|
254
|
+
// ... do work ...
|
|
255
|
+
return someValue; // available as result.result
|
|
256
|
+
});
|
|
257
|
+
if (result.result === "done") break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Conditional sessions
|
|
261
|
+
if (needsReview) {
|
|
262
|
+
await ctx.stage({ name: "review" }, {}, {}, async (s) => { /* ... */ });
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Headless (background) stages
|
|
267
|
+
|
|
268
|
+
Set `headless: true` in the stage options to run the provider SDK
|
|
269
|
+
in-process instead of spawning a tmux window — invisible in the graph,
|
|
270
|
+
identical callback API.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const result = await ctx.stage(
|
|
274
|
+
{ name: "background-task", headless: true },
|
|
275
|
+
{}, {},
|
|
276
|
+
async (s) => {
|
|
277
|
+
const result = await s.session.query("Analyze the codebase.");
|
|
278
|
+
s.save(s.sessionId);
|
|
279
|
+
return extractAssistantText(result, 0);
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
For per-provider mechanics, the canonical fan-out pattern (visible seed →
|
|
285
|
+
parallel headless → visible merge), and topology semantics, see
|
|
286
|
+
`control-flow.md` §"Headless stages: transparent to graph topology" and the
|
|
287
|
+
per-SDK "Headless mode" sections in `agent-sessions.md`. Failure visibility
|
|
288
|
+
caveats live in `failure-modes.md` §F15.
|
|
289
|
+
|
|
290
|
+
## SDK exports
|
|
291
|
+
|
|
292
|
+
The `@bastani/atomic/workflows` package exports the workflow authoring and composition primitives. For native SDK types and utilities, install and import from the provider packages directly.
|
|
293
|
+
|
|
294
|
+
**Composition primitives:**
|
|
295
|
+
- `runWorkflow({ workflow, inputs?, cwd?, detach? })` — spawn a workflow's tmux session on the atomic socket. Resolves with `{ id, tmuxSessionName }` after the session is created (foreground attaches and resolves on detach; `detach: true` returns immediately).
|
|
296
|
+
- `createRegistry()` — factory for an empty, immutable, chainable registry. Chain `.register(wf)` to add workflow definitions. Each call returns a new registry. Throws on duplicate `${agent}/${name}` key.
|
|
297
|
+
- `listWorkflows(registry)` / `getWorkflow(registry, agent, name)` — iterate or look up by `(agent, name)`. Returns `undefined` when the pair isn't registered.
|
|
298
|
+
- `Registry` — type for the registry object (see `registry-and-validation.md`)
|
|
299
|
+
|
|
300
|
+
**Builder:**
|
|
301
|
+
- `defineWorkflow` — entry point; returns a chainable `WorkflowBuilder`. Use `.for("agent")` on the builder to narrow types to a specific provider.
|
|
302
|
+
- `WorkflowBuilder` — the builder class (rarely needed directly)
|
|
303
|
+
|
|
304
|
+
**Session lifecycle (manage running tmux sessions on the shared atomic socket):**
|
|
305
|
+
- `listSessions({ scope?, agent? })` — list every atomic-managed session. Returns `[]` when tmux is not installed.
|
|
306
|
+
- `getSession(id)` — single-session lookup; returns `undefined` when not found.
|
|
307
|
+
- `stopSession(id)` / `detachSession(id)` — best-effort kill / detach all clients. Idempotent.
|
|
308
|
+
- `attachSession(id)` — interactively attach this terminal. Throws `MissingDependencyError` when tmux is missing.
|
|
309
|
+
- `getSessionStatus(id)` — read the on-disk status snapshot for a workflow run; `null` when the orchestrator hasn't written one yet.
|
|
310
|
+
- `getSessionTranscript(id, sessionName)` — read the saved native-message transcript for one stage inside a workflow run.
|
|
311
|
+
|
|
312
|
+
**Pane navigation (pure tmux verbs — never auto-attach):**
|
|
313
|
+
- `nextWindow(id)` / `previousWindow(id)` — move the session's current-window pointer. An attached client sees the change live; a detached session updates silently. Compose with `attachSession(id)` if you want navigate-then-attach.
|
|
314
|
+
- `gotoOrchestrator(id)` — jump to window 0 of the target session. Mirrors the `Ctrl+G` keybinding inside an attached client.
|
|
315
|
+
|
|
316
|
+
**Typed errors (catch with `instanceof` to render friendly CLI output):**
|
|
317
|
+
- `MissingDependencyError` — `dependency: "tmux" | "psmux" | "bun"`. Thrown when a required external dependency is missing on `PATH`.
|
|
318
|
+
- `SessionNotFoundError` — carries `id`. Thrown by `attachSession` and the navigation primitives when the id isn't on the socket.
|
|
319
|
+
- `WorkflowNotCompiledError` — carries `path`. Thrown when a `defineWorkflow(...)` chain is missing `.compile()`.
|
|
320
|
+
- `InvalidWorkflowError` — carries `path`. Thrown when a workflow file's default export isn't a `WorkflowDefinition`.
|
|
321
|
+
- `IncompatibleSDKError` — carries `path`, `requiredVersion`, `currentVersion`. Thrown when `minSDKVersion` is newer than the installed SDK.
|
|
322
|
+
|
|
323
|
+
**Types** (import with `import type`):
|
|
324
|
+
- `AgentType` — `"copilot" | "opencode" | "claude"`
|
|
325
|
+
- `Transcript` — `{ path: string, content: string }` from `ctx.transcript()`
|
|
326
|
+
- `SavedMessage` — union of provider-specific message types
|
|
327
|
+
- `SaveTranscript` — overloaded save function type
|
|
328
|
+
- `SessionContext` — the context object passed to `ctx.stage()` callbacks
|
|
329
|
+
- `SessionHandle<T>` — returned by `ctx.stage()`, carries `{ name, id, result }`
|
|
330
|
+
- `SessionRunOptions` — `{ name, description?, headless? }` for `ctx.stage()` first argument
|
|
331
|
+
- `StageClientOptions<A>` — provider-specific client init options for `ctx.stage()` second argument
|
|
332
|
+
- `StageSessionOptions<A>` — provider-specific session create options for `ctx.stage()` third argument
|
|
333
|
+
- `ProviderClient<A>` — the `s.client` type, resolved by agent type
|
|
334
|
+
- `ProviderSession<A>` — the `s.session` type, resolved by agent type
|
|
335
|
+
- `ClaudeSessionWrapper` — Atomic wrapper for Claude sessions (exposes `s.session.query()`, which returns `SessionMessage[]`)
|
|
336
|
+
- `SessionRef` — `string | SessionHandle<unknown>` for transcript/message lookups
|
|
337
|
+
- `WorkflowContext` — top-level context passed to `.run()` callback
|
|
338
|
+
- `WorkflowOptions` — `{ name, description? }` workflow metadata
|
|
339
|
+
- `WorkflowDefinition` — sealed output of `.compile()`
|
|
340
|
+
|
|
341
|
+
**Response utilities:**
|
|
342
|
+
- `extractAssistantText(messages, afterIndex)` — extract plain text from the `SessionMessage[]` returned by `s.session.query()` for Claude; use `extractAssistantText(result, 0)` to get the full assistant response text
|
|
343
|
+
|
|
344
|
+
**Validation helpers:**
|
|
345
|
+
- `validateClaudeWorkflow` — static validation for Claude workflow source files; warns on direct `createClaudeSession` or `claudeQuery` usage
|
|
346
|
+
- `validateCopilotWorkflow` — static validation for Copilot workflow source files; warns on manual `new CopilotClient` or `client.createSession()` usage
|
|
347
|
+
- `validateOpenCodeWorkflow` — static validation for OpenCode workflow source files; warns on manual `createOpencodeClient()` or `client.session.create()` usage
|
|
348
|
+
|
|
349
|
+
**Native SDK dependencies:**
|
|
350
|
+
|
|
351
|
+
The Atomic runtime provides `s.client` and `s.session` with types resolved from the native SDKs. If you need to name those types in your own code, or use SDK utilities and advanced APIs, import them directly from the provider packages:
|
|
352
|
+
|
|
353
|
+
| Provider | Package | Key imports |
|
|
354
|
+
|----------|---------|-------------|
|
|
355
|
+
| Copilot | `@github/copilot-sdk` | `SessionEvent`, `CopilotClient`, `CopilotSession`, `approveAll`, `defineTool` |
|
|
356
|
+
| Claude | `@anthropic-ai/claude-agent-sdk` | `SessionMessage`, `query` |
|
|
357
|
+
| OpenCode | `@opencode-ai/sdk/v2` | `SessionPromptResponse`, `OpencodeClient`, `Session` |
|
|
358
|
+
|
|
359
|
+
## `SessionContext` reference
|
|
360
|
+
|
|
361
|
+
| Field | Type | Description |
|
|
362
|
+
|-------|------|-------------|
|
|
363
|
+
| `client` | `ProviderClient<A>` | Pre-created SDK client (auto-managed by runtime) |
|
|
364
|
+
| `session` | `ProviderSession<A>` | Pre-created provider session (auto-managed by runtime) |
|
|
365
|
+
| `inputs` | `{ [K in N]?: string }` | Typed inputs for this run — only declared field names are valid keys. Accessing an undeclared field is a compile-time error. See `workflow-inputs.md`. |
|
|
366
|
+
| `agent` | `AgentType` | Which agent is running |
|
|
367
|
+
| `transcript(ref)` | `(ref: SessionRef) => Promise<Transcript>` | Get prior session's transcript as `{ path, content }` |
|
|
368
|
+
| `getMessages(ref)` | `(ref: SessionRef) => Promise<SavedMessage[]>` | Get prior session's raw native messages |
|
|
369
|
+
| `save` | `SaveTranscript` | Save this session's output for downstream sessions |
|
|
370
|
+
| `sessionDir` | `string` | Path to session storage directory |
|
|
371
|
+
| `paneId` | `string` | tmux pane ID (or `headless-<name>-<id>` for headless stages) |
|
|
372
|
+
| `sessionId` | `string` | Session UUID |
|
|
373
|
+
| `stage(opts, clientOpts, sessionOpts, fn)` | `<T>(...) => Promise<SessionHandle<T>>` | Spawn a nested sub-session (child of this session in the graph) |
|
|
374
|
+
|
|
375
|
+
## Reference files
|
|
376
|
+
|
|
377
|
+
The full table of references with load triggers lives in SKILL.md
|
|
378
|
+
§"Reference Files". Pull `failure-modes.md` before shipping any
|
|
379
|
+
multi-session workflow, and `agent-sessions.md` whenever writing SDK calls.
|
|
380
|
+
|
|
381
|
+
## Builtin reference implementations
|
|
382
|
+
|
|
383
|
+
The SDK ships two builtin workflows registered via `createBuiltinRegistry()` (internal to the `atomic` CLI). They demonstrate production patterns for all three SDKs:
|
|
384
|
+
|
|
385
|
+
- **`ralph`** (`src/sdk/workflows/builtin/ralph/`) — iterative plan → orchestrate → review → debug loop with consecutive clean-pass detection, shared helpers for prompts/parsing/git, and cross-SDK adaptation
|
|
386
|
+
- **`deep-research-codebase`** (`src/sdk/workflows/builtin/deep-research-codebase/`) — deterministic codebase scout → LOC-based heuristic explorer partitioning → parallel explorers → aggregator with file-based handoffs and context-aware prompt engineering
|
|
387
|
+
|
|
388
|
+
Both include `helpers/` directories with SDK-agnostic logic (prompt builders, parsers, heuristics) and per-agent `index.ts` files showing how the same workflow topology adapts to Claude, Copilot, and OpenCode. Their composition root pattern (`runWorkflow(via runWorkflow primitives).run()`) is the same pattern user apps follow.
|
|
389
|
+
|
|
390
|
+
## Type safety
|
|
391
|
+
|
|
392
|
+
The SDK avoids `any` and uses `unknown` only at well-defined boundaries (e.g., `SessionRef = string | SessionHandle<unknown>` for handle-erased lookups). `SessionContext` fields are precisely typed, and native provider types may appear inside Atomic generic aliases and runtime values — if you need to name those types in your own code, import them from the provider SDK directly. Use `import type` for type-only imports. Use `.for("agent")` to narrow `s.client` and `s.session` to the correct provider types. Declare `inputs` inline so TypeScript enforces typed access on `ctx.inputs`.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Registry and Validation
|
|
2
|
+
|
|
3
|
+
## `createRegistry()`
|
|
4
|
+
|
|
5
|
+
Factory for an empty, immutable, chainable workflow registry.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createRegistry } from "@bastani/atomic/workflows";
|
|
9
|
+
|
|
10
|
+
const registry = createRegistry()
|
|
11
|
+
.register(myClaudeWorkflow)
|
|
12
|
+
.register(myCopilotWorkflow);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Each `.register()` call returns a **new** registry — the original is unchanged.
|
|
16
|
+
This makes the registry safe to share and compose.
|
|
17
|
+
|
|
18
|
+
## Key scheme
|
|
19
|
+
|
|
20
|
+
Every registered workflow is identified by the composite key:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
${agent}/${name}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Examples: `"claude/ralph"`, `"copilot/gen-spec"`, `"opencode/deep-research-codebase"`.
|
|
27
|
+
|
|
28
|
+
- `agent` — `"claude"` | `"copilot"` | `"opencode"` (set via `.for(agent)` on the builder — pass the agent name as a runtime string argument)
|
|
29
|
+
- `name` — from `defineWorkflow({ name })` — must be non-empty
|
|
30
|
+
|
|
31
|
+
## Validate-on-register
|
|
32
|
+
|
|
33
|
+
`registry.register(wf)` runs provider-specific validation immediately:
|
|
34
|
+
|
|
35
|
+
- **Source validation** — regex checks for anti-patterns in the workflow's
|
|
36
|
+
`.run()` function body (e.g. direct `createClaudeSession` usage instead of
|
|
37
|
+
`s.session.query()`). Warnings are printed to `console.warn`; they do not
|
|
38
|
+
block registration.
|
|
39
|
+
- **Brand check** — the runtime checks `__brand === "WorkflowDefinition"` at
|
|
40
|
+
execution time. Always end the builder chain with `.compile()` to produce
|
|
41
|
+
the correct brand.
|
|
42
|
+
|
|
43
|
+
## Same-name / different-type collision detection
|
|
44
|
+
|
|
45
|
+
Registering the same `${agent}/${name}` key twice throws at registration time:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
[atomic] Duplicate workflow registration: "claude/my-workflow" is already registered.
|
|
49
|
+
Each (agent, name) pair must be unique.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
No silent overwriting. No precedence rules. Pick distinct names.
|
|
53
|
+
|
|
54
|
+
Two workflows with the **same name but different agents** (`"claude/ralph"` and
|
|
55
|
+
`"copilot/ralph"`) are distinct keys and register without conflict — that is the
|
|
56
|
+
intended pattern for cross-agent workflows.
|
|
57
|
+
|
|
58
|
+
## Input flag-name conflicts at `runWorkflow` time
|
|
59
|
+
|
|
60
|
+
`createRegistry()` + `listWorkflows` inspects all registered workflows and builds a
|
|
61
|
+
union of their declared inputs. If two workflows declare the same input name
|
|
62
|
+
with **different types**, `runWorkflow` throws immediately:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
[atomic/worker] Input name conflict: "focus" is declared as "enum" in
|
|
66
|
+
"claude/gen-spec" but as "string" in "copilot/gen-spec".
|
|
67
|
+
Workflows sharing an input name must agree on the type.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Same name + same type: the flag is shared silently (one `--focus` covers
|
|
71
|
+
both workflows).
|
|
72
|
+
|
|
73
|
+
Note: `runWorkflow({ workflow })` is bound to a single workflow, so it
|
|
74
|
+
performs no union. Only the cli faces this class of conflict.
|
|
75
|
+
|
|
76
|
+
## Reserved flag names
|
|
77
|
+
|
|
78
|
+
The following input names are rejected by `defineWorkflow` because they
|
|
79
|
+
conflict with worker CLI flags:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
name, agent, detach, list, help, version
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Attempting to declare an input with one of these names throws at definition time:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
[atomic] Input name "name" is reserved by the worker CLI.
|
|
89
|
+
Rename it. Reserved names: name, agent, detach, list, help, version.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This is enforced at `defineWorkflow` time — it cannot be registered into a
|
|
93
|
+
registry.
|
|
94
|
+
|
|
95
|
+
## `Registry` API
|
|
96
|
+
|
|
97
|
+
| Method | Signature | Behaviour |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `register(wf)` | `(wf: WorkflowDefinition) → Registry` | Returns new registry with wf added. Throws on duplicate key. |
|
|
100
|
+
| `get(key)` | `(key: "${agent}/${name}") → WorkflowDefinition` | Typed retrieval. Throws if key not found. |
|
|
101
|
+
| `has(key)` | `(key: string) → boolean` | Returns `true` if key is registered. |
|
|
102
|
+
| `list()` | `() → readonly WorkflowDefinition[]` | All registered definitions as a frozen array. |
|
|
103
|
+
| `resolve(name, agent)` | `(name, agent) → WorkflowDefinition \| undefined` | Looks up by name + agent pair. Returns `undefined` if not found. |
|
|
104
|
+
|
|
105
|
+
## SDK version compatibility
|
|
106
|
+
|
|
107
|
+
Workflows may opt in to a minimum Atomic CLI version by declaring
|
|
108
|
+
`minSDKVersion` on `defineWorkflow()`. The field is **optional and
|
|
109
|
+
unset by default**.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
defineWorkflow({
|
|
113
|
+
name: "uses-new-stage-option",
|
|
114
|
+
source: import.meta.path,
|
|
115
|
+
minSDKVersion: "0.6.0", // refuse to load on older CLI
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Set it when the workflow calls a newly-added SDK surface. Omit it when
|
|
120
|
+
using stable APIs. An unrecognised or unparseable value is silently
|
|
121
|
+
ignored — the workflow loads as compatible.
|
|
122
|
+
|
|
123
|
+
When the gate trips (`minSDKVersion > installed CLI`), the workflow is
|
|
124
|
+
surfaced as **incompatible** in the list and picker with a visible badge
|
|
125
|
+
(`⚠ needs v<X>`) rather than silently vanishing.
|
|
126
|
+
|
|
127
|
+
## TypeScript configuration
|
|
128
|
+
|
|
129
|
+
Standard module resolution handles all imports. Use `"moduleResolution":
|
|
130
|
+
"bundler"` in `tsconfig.json` (Bun's default). Type-check with:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bun typecheck
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The TypeScript compiler catches:
|
|
137
|
+
- Invalid `SessionContext` / `WorkflowContext` field access
|
|
138
|
+
- Wrong session callback signatures
|
|
139
|
+
- Missing required fields (`name`, `run`)
|
|
140
|
+
- SDK type mismatches (`s.save()` wrong shape)
|
|
141
|
+
- Incorrect provider-specific method calls
|