@bastani/atomic 0.5.12-3 → 0.5.12-5

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 (27) hide show
  1. package/.agents/skills/workflow-creator/SKILL.md +24 -17
  2. package/.agents/skills/workflow-creator/references/agent-sessions.md +67 -24
  3. package/.agents/skills/workflow-creator/references/computation-and-validation.md +5 -3
  4. package/.agents/skills/workflow-creator/references/control-flow.md +25 -11
  5. package/.agents/skills/workflow-creator/references/discovery-and-verification.md +3 -2
  6. package/.agents/skills/workflow-creator/references/failure-modes.md +35 -36
  7. package/.agents/skills/workflow-creator/references/getting-started.md +25 -12
  8. package/.agents/skills/workflow-creator/references/session-config.md +26 -5
  9. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +3 -3
  10. package/.agents/skills/workflow-creator/references/workflow-inputs.md +52 -47
  11. package/README.md +63 -41
  12. package/package.json +2 -4
  13. package/src/commands/cli/workflow.ts +1 -1
  14. package/src/sdk/components/workflow-picker-panel.tsx +109 -47
  15. package/src/sdk/define-workflow.test.ts +58 -0
  16. package/src/sdk/define-workflow.ts +48 -30
  17. package/src/sdk/providers/claude.ts +234 -233
  18. package/src/sdk/runtime/discovery.ts +2 -3
  19. package/src/sdk/runtime/executor.ts +6 -1
  20. package/src/sdk/types.ts +24 -19
  21. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +11 -30
  22. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +7 -4
  23. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +6 -2
  24. package/src/sdk/workflows/builtin/ralph/claude/index.ts +32 -38
  25. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +5 -1
  26. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +5 -1
  27. package/src/sdk/workflows/index.ts +2 -2
@@ -4,7 +4,7 @@ This guide covers the basics of creating workflows with the `defineWorkflow().ru
4
4
 
5
5
  ## Quick-start example
6
6
 
7
- Use `defineWorkflow<"agent">().run(callback).compile()` to define your workflow. 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.
7
+ Use `defineWorkflow({...}).for<"agent">().run(callback).compile()` to define your workflow. 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.
8
8
 
9
9
  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.
10
10
 
@@ -12,15 +12,17 @@ The runtime manages the full session lifecycle automatically — it creates the
12
12
 
13
13
  ```ts
14
14
  // .atomic/workflows/my-workflow/claude/index.ts
15
- import { defineWorkflow } from "@bastani/atomic/workflows";
15
+ import { defineWorkflow, extractAssistantText } from "@bastani/atomic/workflows";
16
16
 
17
- export default defineWorkflow<"claude">({
17
+ export default defineWorkflow({
18
18
  name: "my-workflow",
19
19
  description: "A two-session pipeline",
20
+ inputs: [
21
+ { name: "prompt", type: "text", required: true, description: "task to perform" },
22
+ ],
20
23
  })
24
+ .for<"claude">()
21
25
  .run(async (ctx) => {
22
- // Free-form workflow: the positional CLI prompt lands under
23
- // `inputs.prompt`. Destructure once and close over it in stages.
24
26
  const prompt = ctx.inputs.prompt ?? "";
25
27
 
26
28
  const describe = await ctx.stage(
@@ -55,10 +57,14 @@ export default defineWorkflow<"claude">({
55
57
  // .atomic/workflows/my-workflow/copilot/index.ts
56
58
  import { defineWorkflow } from "@bastani/atomic/workflows";
57
59
 
58
- export default defineWorkflow<"copilot">({
60
+ export default defineWorkflow({
59
61
  name: "my-workflow",
60
62
  description: "A two-session pipeline",
63
+ inputs: [
64
+ { name: "prompt", type: "text", required: true, description: "task to perform" },
65
+ ],
61
66
  })
67
+ .for<"copilot">()
62
68
  .run(async (ctx) => {
63
69
  const prompt = ctx.inputs.prompt ?? "";
64
70
 
@@ -94,10 +100,14 @@ export default defineWorkflow<"copilot">({
94
100
  // .atomic/workflows/my-workflow/opencode/index.ts
95
101
  import { defineWorkflow } from "@bastani/atomic/workflows";
96
102
 
97
- export default defineWorkflow<"opencode">({
103
+ export default defineWorkflow({
98
104
  name: "my-workflow",
99
105
  description: "A two-session pipeline",
106
+ inputs: [
107
+ { name: "prompt", type: "text", required: true, description: "task to perform" },
108
+ ],
100
109
  })
110
+ .for<"opencode">()
101
111
  .run(async (ctx) => {
102
112
  const prompt = ctx.inputs.prompt ?? "";
103
113
 
@@ -172,7 +182,7 @@ const result = await ctx.stage(
172
182
  // s.client, s.session, s.save(), s.transcript() all work identically
173
183
  const result = await s.session.query("Analyze the codebase.");
174
184
  s.save(s.sessionId);
175
- return result.output;
185
+ return extractAssistantText(result, 0);
176
186
  },
177
187
  );
178
188
  // result.result contains the returned value
@@ -208,7 +218,7 @@ Headless stages are transparent to graph topology — `seed → [3 headless] →
208
218
  The `@bastani/atomic/workflows` package exports the workflow authoring primitives. For native SDK types and utilities, install and import from the provider packages directly.
209
219
 
210
220
  **Builder:**
211
- - `defineWorkflow` — entry point, accepts an optional type parameter (`"claude"`, `"copilot"`, `"opencode"`) for type narrowing; returns a chainable `WorkflowBuilder`
221
+ - `defineWorkflow` — entry point; returns a chainable `WorkflowBuilder`. Use `.for<"agent">()` on the builder to narrow types to a specific provider.
212
222
  - `WorkflowBuilder` — the builder class (rarely needed directly)
213
223
 
214
224
  **Types** (import with `import type`):
@@ -223,13 +233,16 @@ The `@bastani/atomic/workflows` package exports the workflow authoring primitive
223
233
  - `StageSessionOptions<A>` — provider-specific session create options for `ctx.stage()` third argument
224
234
  - `ProviderClient<A>` — the `s.client` type, resolved by agent type
225
235
  - `ProviderSession<A>` — the `s.session` type, resolved by agent type
226
- - `ClaudeSessionWrapper` — Atomic wrapper for Claude sessions (exposes `s.session.query()`)
236
+ - `ClaudeSessionWrapper` — Atomic wrapper for Claude sessions (exposes `s.session.query()`, which returns `SessionMessage[]`)
227
237
  - `ClaudeQueryDefaults` — per-stage query defaults (timeouts, poll interval) for Claude sessions
228
238
  - `SessionRef` — `string | SessionHandle<unknown>` for transcript/message lookups
229
239
  - `WorkflowContext` — top-level context passed to `.run()` callback
230
240
  - `WorkflowOptions` — `{ name, description? }` workflow metadata
231
241
  - `WorkflowDefinition` — sealed output of `.compile()`
232
242
 
243
+ **Response utilities:**
244
+ - `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
245
+
233
246
  **Validation helpers:**
234
247
  - `validateClaudeWorkflow` — static validation for Claude workflow source files; warns on direct `createClaudeSession` or `claudeQuery` usage
235
248
  - `validateCopilotWorkflow` — static validation for Copilot workflow source files; warns on manual `new CopilotClient` or `client.createSession()` usage
@@ -251,7 +264,7 @@ The Atomic runtime provides `s.client` and `s.session` with types resolved from
251
264
  |-------|------|-------------|
252
265
  | `client` | `ProviderClient<A>` | Pre-created SDK client (auto-managed by runtime) |
253
266
  | `session` | `ProviderSession<A>` | Pre-created provider session (auto-managed by runtime) |
254
- | `inputs` | `Record<string, string>` | Structured inputs for this run. Free-form workflows read `s.inputs.prompt`; structured workflows read their declared field names. See `workflow-inputs.md`. |
267
+ | `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`. |
255
268
  | `agent` | `AgentType` | Which agent is running |
256
269
  | `transcript(ref)` | `(ref: SessionRef) => Promise<Transcript>` | Get prior session's transcript as `{ path, content }` |
257
270
  | `getMessages(ref)` | `(ref: SessionRef) => Promise<SavedMessage[]>` | Get prior session's raw native messages |
@@ -286,4 +299,4 @@ Both include `helpers/` directories with SDK-agnostic logic (prompt builders, pa
286
299
 
287
300
  ## Type safety
288
301
 
289
- The SDK is typed with **no `unknown` or `any`**. `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 the `defineWorkflow<"agent">()` type parameter to narrow `s.client` and `s.session` to the correct provider types.
302
+ The SDK is typed with **no `unknown` or `any`**. `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`.
@@ -20,11 +20,13 @@ await ctx.stage({ name: "..." }, {
20
20
  ### Session options (`sessionOpts` — 3rd arg to `ctx.stage()`)
21
21
 
22
22
  These are `ClaudeQueryDefaults` and set defaults for every `s.session.query()`
23
- call inside the callback (`timeoutMs`, `pollIntervalMs`, etc.):
23
+ call inside the callback. The available fields are: `pollIntervalMs`,
24
+ `submitPresses`, `maxSubmitRounds`, `readyTimeoutMs`. Note that `timeoutMs` no
25
+ longer exists — idle detection is automatic (pane capture for interactive
26
+ stages, SDK streaming for headless stages).
24
27
 
25
28
  ```ts
26
29
  await ctx.stage({ name: "..." }, {}, {
27
- timeoutMs: 5 * 60 * 1000, // 5 minutes per query (default)
28
30
  pollIntervalMs: 1_000, // Poll interval for output
29
31
  }, async (s) => {
30
32
  await s.session.query((ctx.inputs.prompt ?? ""));
@@ -101,15 +103,34 @@ const result = query({
101
103
  the pane ID from `s.paneId` automatically. Call it inside the stage callback:
102
104
 
103
105
  ```ts
106
+ import { extractAssistantText } from "@anthropic-ai/claude-agent-sdk";
107
+
104
108
  await ctx.stage({ name: "..." }, {}, {}, async (s) => {
105
109
  const result = await s.session.query("Your prompt");
106
- // result.outputcaptured response text
110
+ // extractAssistantText(result, 0) extract assistant text from the result
111
+ const text = extractAssistantText(result, 0);
107
112
  s.save(s.sessionId);
108
113
  });
109
114
  ```
110
115
 
111
- The query defaults (timeout, poll interval) can be configured via `sessionOpts`
112
- as shown above.
116
+ The query defaults (poll interval, submit presses, etc.) can be configured via
117
+ `sessionOpts` as shown above.
118
+
119
+ For **headless stages**, SDK options (such as `permissionMode`, `agent`,
120
+ `allowDangerouslySkipPermissions`) can be passed directly as the second
121
+ argument to `s.session.query()`:
122
+
123
+ ```ts
124
+ await ctx.stage({ name: "..." }, {}, {}, async (s) => {
125
+ const result = await s.session.query("Your prompt", {
126
+ permissionMode: "bypassPermissions",
127
+ allowDangerouslySkipPermissions: true,
128
+ agent: "worker",
129
+ });
130
+ const text = extractAssistantText(result, 0);
131
+ s.save(s.sessionId);
132
+ });
133
+ ```
113
134
 
114
135
  ### Claude hooks
115
136
 
@@ -114,13 +114,13 @@ Use closures and variables for state within a single session:
114
114
  );
115
115
 
116
116
  // Accumulate findings
117
- const review = parseReviewResult(result.output);
117
+ const review = parseReviewResult(extractAssistantText(result, 0));
118
118
  if (review) {
119
119
  findings.push(...review.findings.map(f => f.title));
120
120
  }
121
121
 
122
122
  // Track clean streak
123
- if (!hasActionableFindings(review, result.output)) {
123
+ if (!hasActionableFindings(review, extractAssistantText(result, 0))) {
124
124
  consecutiveClean++;
125
125
  if (consecutiveClean >= 2) break;
126
126
  continue;
@@ -129,7 +129,7 @@ Use closures and variables for state within a single session:
129
129
 
130
130
  // Apply fix
131
131
  const fixResult = await s.session.query(buildFixSpec(review, (ctx.inputs.prompt ?? "")));
132
- priorOutput = fixResult.output;
132
+ priorOutput = extractAssistantText(fixResult, 0);
133
133
  }
134
134
 
135
135
  // All local state is available here
@@ -2,20 +2,22 @@
2
2
 
3
3
  Workflows collect structured data from the user at invocation time through
4
4
  a single uniform API: `ctx.inputs` (and `s.inputs` inside stage
5
- callbacks). This reference covers how the inputs pipe works, when to
6
- declare a schema vs. rely on the free-form fallback, and how values
7
- reach the workflow from the CLI and the interactive picker.
5
+ callbacks). This reference covers how the inputs pipe works, how to
6
+ declare input schemas, and how values reach the workflow from the CLI
7
+ and the interactive picker.
8
8
 
9
9
  ## The inputs pipe
10
10
 
11
- Every workflow run receives a `Record<string, string>` of inputs. The
11
+ Every workflow run receives a typed inputs object. When the workflow
12
+ declares an `inputs` schema, only the declared field names are valid
13
+ keys — accessing undeclared fields is a compile-time error. The
12
14
  runtime populates it from whichever invocation surface the user chose:
13
15
 
14
16
  | Surface | How values are supplied | How they land in `ctx.inputs` |
15
17
  |---|---|---|
16
- | **Named run, positional** — `atomic workflow -n hello -a claude "fix the bug"` | A single positional prompt string | `{ prompt: "fix the bug" }` |
18
+ | **Named run, positional** — `atomic workflow -n hello -a claude "fix the bug"` | A single positional prompt string (the workflow must declare a `prompt` input) | `{ prompt: "fix the bug" }` |
17
19
  | **Named run, structured** — `atomic workflow -n gen-spec -a claude --research_doc=notes.md --focus=standard` | One `--<field>=<value>` flag per declared input | `{ research_doc: "notes.md", focus: "standard" }` |
18
- | **Interactive picker** — `atomic workflow -a claude` | The user fills in a form rendered from the declared schema (or the default `prompt` field if the workflow is free-form) | Whatever the user typed, keyed by field name |
20
+ | **Interactive picker** — `atomic workflow -a claude` | The user fills in a form rendered from the declared schema | Whatever the user typed, keyed by field name |
19
21
 
20
22
  Workflow code is the same either way — it always reads
21
23
  `ctx.inputs.<name>`. The invocation surface is a CLI concern, not a
@@ -23,12 +25,19 @@ workflow concern.
23
25
 
24
26
  ## Reading inputs
25
27
 
26
- For free-form workflows (no declared schema), the positional prompt
27
- lands under the reserved `prompt` key. Destructure it once at the top of
28
- `.run()` so every stage can close over a bare string:
28
+ Workflows that accept a user prompt should declare it explicitly as an
29
+ input. Destructure it once at the top of `.run()` so every stage can
30
+ close over a bare string:
29
31
 
30
32
  ```ts
31
- defineWorkflow<"claude">({ name: "answer", description: "Single-turn answer" })
33
+ defineWorkflow({
34
+ name: "answer",
35
+ description: "Single-turn answer",
36
+ inputs: [
37
+ { name: "prompt", type: "text", required: true, description: "question to answer" },
38
+ ],
39
+ })
40
+ .for<"claude">()
32
41
  .run(async (ctx) => {
33
42
  const prompt = ctx.inputs.prompt ?? "";
34
43
 
@@ -45,7 +54,7 @@ out of `ctx.inputs` once for readability and so downstream stages can
45
54
  close over locals:
46
55
 
47
56
  ```ts
48
- defineWorkflow<"claude">({
57
+ defineWorkflow({
49
58
  name: "gen-spec",
50
59
  description: "Convert a research doc into a detailed execution spec",
51
60
  inputs: [
@@ -60,6 +69,7 @@ defineWorkflow<"claude">({
60
69
  { name: "notes", type: "text" },
61
70
  ],
62
71
  })
72
+ .for<"claude">()
63
73
  .run(async (ctx) => {
64
74
  const { research_doc, focus } = ctx.inputs;
65
75
  const notes = ctx.inputs.notes ?? "";
@@ -99,7 +109,7 @@ interface WorkflowInput {
99
109
  /** Default value — enums use this to pick their initial value. */
100
110
  default?: string;
101
111
  /** Allowed values — required when `type` is `"enum"`. */
102
- values?: string[];
112
+ values?: readonly string[];
103
113
  }
104
114
  ```
105
115
 
@@ -147,35 +157,29 @@ This validation runs before any workflow code, so a malformed
147
157
  invocation can never reach your `.run()` callback in a half-filled
148
158
  state.
149
159
 
150
- ## Free-form vs structured: when to use which
160
+ ## Declaring a prompt input
151
161
 
152
- Choose **free-form** (no `inputs` field on `defineWorkflow`) when:
162
+ Workflows that accept a user prompt should declare it explicitly in their
163
+ `inputs` array rather than relying on an implicit key:
153
164
 
154
- - The workflow takes a single unstructured request that varies widely
155
- in phrasing — "find the bug", "build me a chart", "refactor the auth
156
- module".
157
- - You want the simplest possible CLI surface.
158
- - The workflow's first LLM call will do its own intent extraction from
159
- the raw prompt.
160
-
161
- Read the prompt via `ctx.inputs.prompt ?? ""`.
165
+ ```ts
166
+ inputs: [
167
+ { name: "prompt", type: "text", required: true, description: "task to perform" },
168
+ ]
169
+ ```
162
170
 
163
- Choose **structured** (declared `inputs: [...]`) when:
171
+ This gives the same CLI ergonomics — `atomic workflow -n hello -a claude "fix the bug"` still works — while providing compile-time safety. Accessing `ctx.inputs.prompt` without declaring it is a type error.
164
172
 
165
- - Several distinct fields are always needed — a file path + a focus
166
- level + optional notes, for example.
167
- - You want the picker to show a real form (each field, its type, and
168
- any validation cues) instead of a single blob text area.
169
- - You want the CLI to reject bad inputs before spawning a workflow —
170
- e.g. a nonexistent enum value.
171
- - You want the invocation to be scriptable and auditable — flag-based
172
- invocation reads cleanly in CI.
173
+ For workflows that need both a free-form prompt AND structured parameters,
174
+ declare all fields in the schema:
173
175
 
174
- Structured workflows can also include a `prompt` field in their schema
175
- if they need both a free-form request AND structured parameters. The
176
- `prompt` key is not magic for structured workflows it's just a
177
- conventional name. You only get "positional maps to `prompt`" behavior
178
- when the workflow has no schema at all.
176
+ ```ts
177
+ inputs: [
178
+ { name: "prompt", type: "text", required: true, description: "what to build" },
179
+ { name: "focus", type: "enum", required: true, values: ["minimal", "standard", "exhaustive"], default: "standard" },
180
+ { name: "notes", type: "text", description: "extra context" },
181
+ ]
182
+ ```
179
183
 
180
184
  ## The interactive picker
181
185
 
@@ -187,8 +191,7 @@ picker. The picker:
187
191
  2. Loads each workflow's metadata (description + declared inputs).
188
192
  3. Shows a Telescope-style fuzzy list. The user types to filter,
189
193
  arrows to navigate, ↵ to lock in a selection.
190
- 4. Renders the selected workflow's form. Free-form workflows get a
191
- single `prompt` text field; structured workflows get one field
194
+ 4. Renders the selected workflow's form. The picker renders one field
192
195
  per declared input with type-specific rendering.
193
196
  5. Validates required fields on ⌃s. If any are empty, focus jumps to
194
197
  the first invalid field and the run button stays disabled.
@@ -246,18 +249,20 @@ Both `--flag=value` and `--flag value` forms are accepted. Short flags
246
249
 
247
250
  ## Pitfalls
248
251
 
249
- ### Don't expect `inputs.prompt` on structured workflows
252
+ ### Declare every field you access
250
253
 
251
- A structured workflow that doesn't declare a `prompt` field will have
252
- `ctx.inputs.prompt === undefined`. The CLI also rejects a positional
253
- prompt for structured workflows outright — if you try
254
- `atomic workflow -n gen-spec -a claude "some text"` you'll get:
254
+ With typed inputs, accessing `ctx.inputs.foo` when `foo` is not declared
255
+ in the workflow's `inputs` array is a compile-time error. If your workflow
256
+ needs a prompt field, declare it:
255
257
 
256
- > Error: workflow 'gen-spec' takes structured inputs — pass them as
257
- > `--<name>=<value>` flags instead of a positional prompt.
258
+ ```ts
259
+ inputs: [
260
+ { name: "prompt", type: "text", required: true, description: "task prompt" },
261
+ ]
262
+ ```
258
263
 
259
- If your workflow wants both a free-form prompt AND structured fields,
260
- declare `prompt` as a `text` input explicitly in the schema.
264
+ The CLI rejects positional prompt strings for workflows that don't declare
265
+ a `prompt` input.
261
266
 
262
267
  ### Don't rename inputs across workflow versions
263
268
 
package/README.md CHANGED
@@ -78,8 +78,9 @@ Each of these is a `.ts` file using Atomic's [Workflow SDK](#workflow-sdk--build
78
78
  - [Saving Transcripts](#saving-transcripts)
79
79
  - [Per-Agent Session APIs](#per-agent-session-apis)
80
80
  - [Key Rules](#key-rules)
81
- - [Deep Codebase Research](#deep-codebase-research)
81
+ - [Research Codebase](#research-codebase)
82
82
  - [Autonomous Execution (Ralph)](#autonomous-execution-ralph)
83
+ - [Deep Research Codebase](#deep-research-codebase)
83
84
  - [Containerized Execution](#containerized-execution)
84
85
  - [Specialized Sub-Agents](#specialized-sub-agents)
85
86
  - [Built-in Skills](#built-in-skills)
@@ -258,10 +259,10 @@ Here's one of the [canonical use cases](#what-you-can-build) — a team pipeline
258
259
  // .atomic/workflows/review-to-merge/claude/index.ts
259
260
  import { defineWorkflow } from "@bastani/atomic/workflows";
260
261
 
261
- export default defineWorkflow<"claude">({
262
+ export default defineWorkflow({
262
263
  name: "review-to-merge",
263
264
  description: "Review → CI → PR → Notify → Approve → Merge",
264
- })
265
+ }).for<"claude">()
265
266
  .run(async (ctx) => {
266
267
  // Step 1: Review the changes
267
268
  const review = await ctx.stage(
@@ -368,10 +369,11 @@ atomic workflow -n my-workflow -a claude "describe this project"
368
369
  // .atomic/workflows/my-workflow/claude/index.ts
369
370
  import { defineWorkflow } from "@bastani/atomic/workflows";
370
371
 
371
- export default defineWorkflow<"claude">({
372
+ export default defineWorkflow({
372
373
  name: "my-workflow",
373
374
  description: "Two-session pipeline: describe -> summarize",
374
- })
375
+ inputs: [{ name: "prompt", type: "text", required: true, description: "task prompt" }],
376
+ }).for<"claude">()
375
377
  .run(async (ctx) => {
376
378
  const prompt = ctx.inputs.prompt ?? "";
377
379
 
@@ -407,10 +409,11 @@ export default defineWorkflow<"claude">({
407
409
  ```ts
408
410
  import { defineWorkflow } from "@bastani/atomic/workflows";
409
411
 
410
- export default defineWorkflow<"claude">({
412
+ export default defineWorkflow({
411
413
  name: "parallel-demo",
412
414
  description: "describe -> [summarize-a, summarize-b] -> merge",
413
- })
415
+ inputs: [{ name: "prompt", type: "text", required: true, description: "task prompt" }],
416
+ }).for<"claude">()
414
417
  .run(async (ctx) => {
415
418
  const prompt = ctx.inputs.prompt ?? "";
416
419
 
@@ -458,7 +461,7 @@ Declare an `inputs` array on `defineWorkflow` and the CLI materialises one `--<f
458
461
  // .atomic/workflows/gen-spec/claude/index.ts
459
462
  import { defineWorkflow } from "@bastani/atomic/workflows";
460
463
 
461
- export default defineWorkflow<"claude">({
464
+ export default defineWorkflow({
462
465
  name: "gen-spec",
463
466
  description: "Convert a research doc into an execution spec",
464
467
  inputs: [
@@ -483,7 +486,7 @@ export default defineWorkflow<"claude">({
483
486
  description: "extra guidance for the spec writer (optional)",
484
487
  },
485
488
  ],
486
- })
489
+ }).for<"claude">()
487
490
  .run(async (ctx) => {
488
491
  // Read each declared field by name.
489
492
  const { research_doc, focus } = ctx.inputs;
@@ -520,12 +523,13 @@ atomic workflow -a claude
520
523
  Stages can run in **headless mode** (`headless: true`) — they execute the provider SDK in-process instead of spawning a tmux window. Headless stages are invisible in the workflow graph but tracked via a background task counter in the statusline. Use them for parallel data-gathering tasks that don't need a visible TUI.
521
524
 
522
525
  ```ts
523
- import { defineWorkflow } from "@bastani/atomic/workflows";
526
+ import { defineWorkflow, extractAssistantText } from "@bastani/atomic/workflows";
524
527
 
525
- export default defineWorkflow<"claude">({
528
+ export default defineWorkflow({
526
529
  name: "headless-demo",
527
530
  description: "seed -> [3 headless background] -> merge",
528
- })
531
+ inputs: [{ name: "prompt", type: "text", required: true, description: "task prompt" }],
532
+ }).for<"claude">()
529
533
  .run(async (ctx) => {
530
534
  const prompt = ctx.inputs.prompt ?? "";
531
535
 
@@ -536,7 +540,7 @@ export default defineWorkflow<"claude">({
536
540
  async (s) => {
537
541
  const result = await s.session.query(prompt);
538
542
  s.save(s.sessionId);
539
- return String(result.output ?? "");
543
+ return extractAssistantText(result, 0);
540
544
  },
541
545
  );
542
546
 
@@ -545,17 +549,17 @@ export default defineWorkflow<"claude">({
545
549
  ctx.stage({ name: "pros", headless: true }, {}, {}, async (s) => {
546
550
  const r = await s.session.query(`List 3 pros:\n\n${seed.result}`);
547
551
  s.save(s.sessionId);
548
- return String(r.output ?? "");
552
+ return extractAssistantText(r, 0);
549
553
  }),
550
554
  ctx.stage({ name: "cons", headless: true }, {}, {}, async (s) => {
551
555
  const r = await s.session.query(`List 3 cons:\n\n${seed.result}`);
552
556
  s.save(s.sessionId);
553
- return String(r.output ?? "");
557
+ return extractAssistantText(r, 0);
554
558
  }),
555
559
  ctx.stage({ name: "uses", headless: true }, {}, {}, async (s) => {
556
560
  const r = await s.session.query(`List 3 use cases:\n\n${seed.result}`);
557
561
  s.save(s.sessionId);
558
- return String(r.output ?? "");
562
+ return extractAssistantText(r, 0);
559
563
  }),
560
564
  ]);
561
565
 
@@ -625,29 +629,29 @@ Use your workflow-creator skill to create a workflow that plans, implements, and
625
629
 
626
630
  #### WorkflowContext (`ctx`) — top-level orchestrator
627
631
 
628
- | Property | Type | Description |
629
- | ---------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
630
- | `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 |
631
- | `ctx.agent` | `AgentType` | Which agent is running (`"claude"`, `"copilot"`, `"opencode"`) |
632
- | `ctx.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a session — returns handle with `name`, `id`, `result` |
633
- | `ctx.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript (`{ path, content }`) |
634
- | `ctx.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
632
+ | Property | Type | Description |
633
+ | ---------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
634
+ | `ctx.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. Workflows that need a prompt must declare it in their `inputs` schema |
635
+ | `ctx.agent` | `AgentType` | Which agent is running (`"claude"`, `"copilot"`, `"opencode"`) |
636
+ | `ctx.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a session — returns handle with `name`, `id`, `result` |
637
+ | `ctx.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript (`{ path, content }`) |
638
+ | `ctx.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
635
639
 
636
640
  #### SessionContext (`s`) — inside each session callback
637
641
 
638
- | Property | Type | Description |
639
- | -------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
640
- | `s.client` | `ProviderClient<A>` | Pre-created SDK client (auto-managed by runtime) |
641
- | `s.session` | `ProviderSession<A>` | Pre-created provider session (auto-managed by runtime) |
642
- | `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` |
643
- | `s.agent` | `AgentType` | Which agent is running |
644
- | `s.paneId` | `string` | tmux pane ID for this session |
645
- | `s.sessionId` | `string` | Session UUID |
646
- | `s.sessionDir` | `string` | Path to this session's storage directory on disk |
647
- | `s.save(messages)` | `SaveTranscript` | Save this session's output for subsequent sessions |
648
- | `s.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript |
649
- | `s.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
650
- | `s.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a nested sub-session (child in the graph) |
642
+ | Property | Type | Description |
643
+ | -------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
644
+ | `s.client` | `ProviderClient<A>` | Pre-created SDK client (auto-managed by runtime) |
645
+ | `s.session` | `ProviderSession<A>` | Pre-created provider session (auto-managed by runtime) |
646
+ | `s.inputs` | `{ [K in N]?: string }` | Same typed inputs as `ctx.inputs`, forwarded into every stage so session callbacks can read values without closing over the outer `ctx` |
647
+ | `s.agent` | `AgentType` | Which agent is running |
648
+ | `s.paneId` | `string` | tmux pane ID for this session |
649
+ | `s.sessionId` | `string` | Session UUID |
650
+ | `s.sessionDir` | `string` | Path to this session's storage directory on disk |
651
+ | `s.save(messages)` | `SaveTranscript` | Save this session's output for subsequent sessions |
652
+ | `s.transcript(ref)` | `Promise<Transcript>` | Get a completed session's transcript |
653
+ | `s.getMessages(ref)` | `Promise<SavedMessage[]>` | Get a completed session's raw native messages |
654
+ | `s.stage(opts, clientOpts, sessionOpts, fn)` | `Promise<SessionHandle<T>>` | Spawn a nested sub-session (child in the graph) |
651
655
 
652
656
  #### Session Options (`SessionRunOptions`)
653
657
 
@@ -691,9 +695,12 @@ The runtime auto-creates `s.client` and `s.session` — use them directly inside
691
695
 
692
696
  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/`.
693
697
 
698
+ > [!TIP]
699
+ > **Keeping workflows up to date:** When the Workflow SDK is updated (new return types, new options, deprecated patterns), you can ask the `workflow-creator` skill to migrate your existing workflows to the latest best practices. Just open your workflow file and ask: _"Update this workflow to use the latest SDK patterns."_ The skill stays current with the SDK and will apply the right changes automatically.
700
+
694
701
  </details>
695
702
 
696
- ### Deep Codebase Research
703
+ ### Research Codebase
697
704
 
698
705
  The `/research-codebase` command dispatches **specialized sub-agents in parallel** to analyze your codebase:
699
706
 
@@ -752,7 +759,7 @@ Research outputs persist in your `research/` directory and specs persist in your
752
759
  <img src="assets/ralph-wiggum.jpg" alt="Ralph Wiggum" width="600">
753
760
  </p>
754
761
 
755
- The [Ralph Wiggum Method](https://ghuntley.com/ralph/) enables **multi-hour autonomous coding sessions**. After approving your spec, let Ralph work in the background while you focus on other tasks.
762
+ The [Ralph Method](https://ghuntley.com/ralph/) enables **multi-hour autonomous coding sessions**. After approving your spec, let Ralph work in the background while you focus on other tasks.
756
763
 
757
764
  **How Ralph works:**
758
765
 
@@ -778,6 +785,21 @@ cd ../my-project-ralph
778
785
  atomic workflow -n ralph -a claude "Build the auth module"
779
786
  ```
780
787
 
788
+ ### Deep Research Codebase
789
+
790
+ Atomic also ships with `deep-research-codebase`, a built-in workflow that performs **multi-agent parallel research** across your codebase. While `/research-codebase` is a single-shot command, the `deep-research-codebase` workflow is a full multi-stage pipeline:
791
+
792
+ 1. **Scout** — A single agent scans the codebase structure and produces an architectural orientation
793
+ 2. **History** — A parallel agent surfaces prior research from `research/docs/`
794
+ 3. **Explorers** — Multiple parallel agents (count scaled by LOC) each investigate a partition of the codebase, writing findings to scratch files
795
+ 4. **Aggregator** — A final agent synthesizes all explorer reports + history into a dated research document at `research/docs/YYYY-MM-DD-<slug>.md`
796
+
797
+ ```bash
798
+ atomic workflow -n deep-research-codebase -a claude "How does the authentication system work?"
799
+ ```
800
+
801
+ The workflow produces a permanent research artifact that can be referenced by future runs, specs, or other workflows.
802
+
781
803
  ### Containerized Execution
782
804
 
783
805
  Atomic ships as **devcontainer features** that bundle the CLI, agent, and all dependencies into isolated containers. This is the recommended way to run autonomous agents safely.
@@ -1043,7 +1065,7 @@ atomic chat -a claude --verbose # Forward --verbose to claude
1043
1065
  | `-n, --name <name>` | Workflow name (matches directory under `.atomic/workflows/<name>/`) |
1044
1066
  | `-a, --agent <name>` | Agent: `claude`, `opencode`, `copilot` |
1045
1067
  | `--<field>=<value>` | Structured input for workflows that declare an `inputs` schema (also accepts `--<field> <value>`) |
1046
- | `[prompt...]` | Positional prompt for free-form workflows (rejected on workflows with a declared schema) |
1068
+ | `[prompt...]` | Positional prompt requires the workflow to declare a `prompt` input |
1047
1069
 
1048
1070
  The workflow command supports four invocation shapes:
1049
1071
 
@@ -1057,7 +1079,7 @@ atomic workflow list -a claude # filter by agent
1057
1079
  # and confirm with y/n
1058
1080
  atomic workflow -a claude
1059
1081
 
1060
- # 3. Run a free-form workflow with a positional prompt
1082
+ # 3. Run a workflow with a positional prompt (workflow must declare a "prompt" input)
1061
1083
  atomic workflow -n ralph -a claude "build a REST API for user management"
1062
1084
 
1063
1085
  # 4. Run a structured-input workflow with one --<field> flag per declared input
@@ -1066,7 +1088,7 @@ atomic workflow -n gen-spec -a claude \
1066
1088
  --focus=standard
1067
1089
  ```
1068
1090
 
1069
- 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.
1091
+ 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 declare a `prompt` input accept a positional prompt on the command line, 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.
1070
1092
 
1071
1093
  #### `atomic completions` — Shell Completions
1072
1094
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.12-3",
3
+ "version": "0.5.12-5",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,7 +71,7 @@
71
71
  "typescript-language-server": "^5.1.3"
72
72
  },
73
73
  "dependencies": {
74
- "@anthropic-ai/claude-agent-sdk": "^0.2.105",
74
+ "@anthropic-ai/claude-agent-sdk": "^0.2.108",
75
75
  "@clack/prompts": "^1.2.0",
76
76
  "@commander-js/extra-typings": "^14.0.0",
77
77
  "@github/copilot-sdk": "^0.2.2",
@@ -81,8 +81,6 @@
81
81
  "commander": "^14.0.3",
82
82
  "ignore": "^7.0.5",
83
83
  "react": "^19.2.5",
84
- "react-devtools-core": "^7.0.1",
85
- "ws": "^8.18.0",
86
84
  "yaml": "^2.8.3",
87
85
  "zod": "^4.3.6"
88
86
  }