@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
package/src/sdk/types.ts CHANGED
@@ -175,7 +175,7 @@ export interface WorkflowInput {
175
175
  /** Default value pre-filled into the field. Enums use this to pick their initial value. */
176
176
  default?: string;
177
177
  /** Allowed values — required when `type` is `"enum"`. */
178
- values?: string[];
178
+ values?: readonly string[];
179
179
  }
180
180
 
181
181
  // ─── Core types ─────────────────────────────────────────────────────────────
@@ -254,7 +254,7 @@ export interface SessionRunOptions {
254
254
  * Created by `ctx.stage(opts, clientOpts, sessionOpts, fn)` — the callback
255
255
  * receives this as its argument with pre-initialized `client` and `session`.
256
256
  */
257
- export interface SessionContext<A extends AgentType = AgentType> {
257
+ export interface SessionContext<A extends AgentType = AgentType, N extends string = string> {
258
258
  /** Provider-specific SDK client (auto-created by runtime) */
259
259
  client: ProviderClient<A>;
260
260
  /** Provider-specific session (auto-created by runtime) */
@@ -263,12 +263,12 @@ export interface SessionContext<A extends AgentType = AgentType> {
263
263
  * Structured inputs for this workflow run. Populated from CLI flags
264
264
  * (`--<name>=<value>`) or the interactive picker.
265
265
  *
266
- * Free-form workflows (no declared `inputs` schema) receive their
267
- * single positional prompt under the `prompt` key so
268
- * `s.inputs.prompt` is the canonical way to read the user's prompt
269
- * regardless of whether the workflow is structured or free-form.
266
+ * When the workflow declares an `inputs` schema, only the declared
267
+ * field names are valid keys accessing undeclared fields is a
268
+ * compile-time error. Free-form workflows (no declared schema)
269
+ * allow any key, including the conventional `prompt` key.
270
270
  */
271
- inputs: Record<string, string>;
271
+ inputs: { [K in N]?: string };
272
272
  /** Which agent is running */
273
273
  agent: A;
274
274
  /**
@@ -301,7 +301,7 @@ export interface SessionContext<A extends AgentType = AgentType> {
301
301
  options: SessionRunOptions,
302
302
  clientOpts: StageClientOptions<A>,
303
303
  sessionOpts: StageSessionOptions<A>,
304
- run: (ctx: SessionContext<A>) => Promise<T>,
304
+ run: (ctx: SessionContext<A, N>) => Promise<T>,
305
305
  ): Promise<SessionHandle<T>>;
306
306
  }
307
307
 
@@ -309,17 +309,17 @@ export interface SessionContext<A extends AgentType = AgentType> {
309
309
  * Top-level context provided to the workflow's `.run()` callback.
310
310
  * Does not have session-specific fields (paneId, save, etc.).
311
311
  */
312
- export interface WorkflowContext<A extends AgentType = AgentType> {
312
+ export interface WorkflowContext<A extends AgentType = AgentType, N extends string = string> {
313
313
  /**
314
314
  * Structured inputs for this workflow run. Populated from CLI flags
315
315
  * (`--<name>=<value>`) or the interactive picker.
316
316
  *
317
- * Free-form workflows (no declared `inputs` schema) receive their
318
- * single positional prompt under the `prompt` key so
319
- * `ctx.inputs.prompt` is the canonical way to read the user's prompt
320
- * regardless of whether the workflow is structured or free-form.
317
+ * When the workflow declares an `inputs` schema, only the declared
318
+ * field names are valid keys accessing undeclared fields is a
319
+ * compile-time error. Free-form workflows (no declared schema)
320
+ * allow any key, including the conventional `prompt` key.
321
321
  */
322
- inputs: Record<string, string>;
322
+ inputs: { [K in N]?: string };
323
323
  /** Which agent is running */
324
324
  agent: A;
325
325
  /**
@@ -332,7 +332,7 @@ export interface WorkflowContext<A extends AgentType = AgentType> {
332
332
  options: SessionRunOptions,
333
333
  clientOpts: StageClientOptions<A>,
334
334
  sessionOpts: StageSessionOptions<A>,
335
- run: (ctx: SessionContext<A>) => Promise<T>,
335
+ run: (ctx: SessionContext<A, N>) => Promise<T>,
336
336
  ): Promise<SessionHandle<T>>;
337
337
  /**
338
338
  * Get a completed session's transcript as rendered text.
@@ -349,7 +349,9 @@ export interface WorkflowContext<A extends AgentType = AgentType> {
349
349
  /**
350
350
  * Options for defining a workflow.
351
351
  */
352
- export interface WorkflowOptions {
352
+ export interface WorkflowOptions<
353
+ I extends readonly WorkflowInput[] = readonly WorkflowInput[],
354
+ > {
353
355
  /** Unique workflow name */
354
356
  name: string;
355
357
  /** Human-readable description */
@@ -359,19 +361,22 @@ export interface WorkflowOptions {
359
361
  * `--<name>` flag per entry and the interactive picker renders one form
360
362
  * field per entry. Leave unset to keep the workflow free-form (a single
361
363
  * positional prompt argument).
364
+ *
365
+ * Write the array inline so TypeScript can infer literal input names
366
+ * and enforce them on `ctx.inputs`.
362
367
  */
363
- inputs?: WorkflowInput[];
368
+ inputs?: I;
364
369
  }
365
370
 
366
371
  /**
367
372
  * A compiled workflow definition — the sealed output of defineWorkflow().compile().
368
373
  */
369
- export interface WorkflowDefinition<A extends AgentType = AgentType> {
374
+ export interface WorkflowDefinition<A extends AgentType = AgentType, N extends string = string> {
370
375
  readonly __brand: "WorkflowDefinition";
371
376
  readonly name: string;
372
377
  readonly description: string;
373
378
  /** Declared input schema — empty array for free-form workflows. */
374
379
  readonly inputs: readonly WorkflowInput[];
375
380
  /** The workflow's entry point. Called by the executor with a WorkflowContext. */
376
- readonly run: (ctx: WorkflowContext<A>) => Promise<void>;
381
+ readonly run: (ctx: WorkflowContext<A, N>) => Promise<void>;
377
382
  }
@@ -79,32 +79,23 @@ import {
79
79
  slugifyPrompt,
80
80
  } from "../helpers/prompts.ts";
81
81
 
82
- // ── Timeouts ────────────────────────────────────────────────────────────────
83
- // Every s.session.query() call passes one of these explicitly never relying
84
- // on the 300-second default. Explorer and aggregator stages dispatch sub-agents
85
- // and can easily run 30+ minutes; a premature timeout causes the stage to
86
- // complete early, which makes Promise.all resolve and the next stage to launch
87
- // before parallel stages finish.
88
- const SCOUT_TIMEOUT_MS = 15 * 60 * 1000; // 15 min — short orientation call
89
- const HISTORY_TIMEOUT_MS = 20 * 60 * 1000; // 20 min — reads research/ docs
90
- const EXPLORER_TIMEOUT_MS = 45 * 60 * 1000; // 45 min — multi-step sub-agent dispatch
91
- const AGGREGATOR_TIMEOUT_MS = 45 * 60 * 1000; // 45 min — reads N explorer reports
82
+ // ── Idle detection ─────────────────────────────────────────────────────────
83
+ // Completion is detected by watching the session JSONL file for idle and result
84
+ // events from Claude's own SDK no manual timeout is needed. The loop runs
85
+ // until Claude reports idle or a result (success, error_max_turns, etc.).
92
86
 
93
- // Between sub-agent dispatches Claude's TUI briefly shows the prompt indicator
94
- // without an active-task spinner. Requiring 3 consecutive idle detections
95
- // prevents the query from returning during these transient gaps.
96
- const EXPLORER_IDLE_CONFIRM = 3;
97
- const AGGREGATOR_IDLE_CONFIRM = 3;
98
-
99
- export default defineWorkflow<"claude">({
87
+ export default defineWorkflow({
100
88
  name: "deep-research-codebase",
101
89
  description:
102
90
  "Deterministic deep codebase research: scout → LOC-driven parallel explorers → aggregator",
91
+ inputs: [
92
+ { name: "prompt", type: "text", required: true, description: "research question" },
93
+ ],
103
94
  })
95
+ .for<"claude">()
104
96
  .run(async (ctx) => {
105
- // Free-form workflows receive their positional prompt under
106
- // `inputs.prompt`; destructure once so every stage below can close
107
- // over a bare `prompt` string without re-reaching into ctx.inputs.
97
+ // Destructure once so every stage below can close over a bare
98
+ // `prompt` string without re-reaching into ctx.inputs.
108
99
  const prompt = ctx.inputs.prompt ?? "";
109
100
  const root = getCodebaseRoot();
110
101
  const startedAt = new Date();
@@ -166,7 +157,6 @@ export default defineWorkflow<"claude">({
166
157
  explorerCount: actualCount,
167
158
  partitionPreview: partitions,
168
159
  }),
169
- { timeoutMs: SCOUT_TIMEOUT_MS },
170
160
  );
171
161
  s.save(s.sessionId);
172
162
 
@@ -195,7 +185,6 @@ export default defineWorkflow<"claude">({
195
185
  // synthesis as prose (no file write — consumed via transcript).
196
186
  await s.session.query(
197
187
  buildHistoryPrompt({ question: prompt, root }),
198
- { timeoutMs: HISTORY_TIMEOUT_MS },
199
188
  );
200
189
  s.save(s.sessionId);
201
190
  },
@@ -255,10 +244,6 @@ export default defineWorkflow<"claude">({
255
244
  scratchPath,
256
245
  root,
257
246
  }),
258
- {
259
- timeoutMs: EXPLORER_TIMEOUT_MS,
260
- idleConfirmCount: EXPLORER_IDLE_CONFIRM,
261
- },
262
247
  );
263
248
  s.save(s.sessionId);
264
249
 
@@ -309,10 +294,6 @@ export default defineWorkflow<"claude">({
309
294
  scoutOverview,
310
295
  historyOverview,
311
296
  }),
312
- {
313
- timeoutMs: AGGREGATOR_TIMEOUT_MS,
314
- idleConfirmCount: AGGREGATOR_IDLE_CONFIRM,
315
- },
316
297
  );
317
298
  s.save(s.sessionId);
318
299
  },
@@ -63,15 +63,18 @@ import {
63
63
  slugifyPrompt,
64
64
  } from "../helpers/prompts.ts";
65
65
 
66
- export default defineWorkflow<"copilot">({
66
+ export default defineWorkflow({
67
67
  name: "deep-research-codebase",
68
68
  description:
69
69
  "Deterministic deep codebase research: scout → LOC-driven parallel explorers → aggregator",
70
+ inputs: [
71
+ { name: "prompt", type: "text", required: true, description: "research question" },
72
+ ],
70
73
  })
74
+ .for<"copilot">()
71
75
  .run(async (ctx) => {
72
- // Free-form workflows receive their positional prompt under
73
- // `inputs.prompt`; destructure once so every stage below can close
74
- // over a bare `prompt` string without re-reaching into ctx.inputs.
76
+ // Destructure once so every stage below can close over a bare
77
+ // `prompt` string without re-reaching into ctx.inputs.
75
78
  const prompt = ctx.inputs.prompt ?? "";
76
79
  const root = getCodebaseRoot();
77
80
  const startedAt = new Date();
@@ -66,13 +66,17 @@ import {
66
66
  slugifyPrompt,
67
67
  } from "../helpers/prompts.ts";
68
68
 
69
- export default defineWorkflow<"opencode">({
69
+ export default defineWorkflow({
70
70
  name: "deep-research-codebase",
71
71
  description:
72
72
  "Deterministic deep codebase research: scout → LOC-driven parallel explorers → aggregator",
73
+ inputs: [
74
+ { name: "prompt", type: "text", required: true, description: "research question" },
75
+ ],
73
76
  })
77
+ .for<"opencode">()
74
78
  .run(async (ctx) => {
75
- // Free-form workflows receive their positional prompt under
79
+ // Destructure once so every stage below can close over a bare
76
80
  // `inputs.prompt`; destructure once so every stage below can close
77
81
  // over a bare `prompt` string without re-reaching into ctx.inputs.
78
82
  const prompt = ctx.inputs.prompt ?? "";
@@ -14,7 +14,7 @@
14
14
  * Run: atomic workflow -n ralph -a claude "<your spec>"
15
15
  */
16
16
 
17
- import { defineWorkflow } from "../../../index.ts";
17
+ import { defineWorkflow, extractAssistantText } from "../../../index.ts";
18
18
  import { query as claudeSdkQuery } from "@anthropic-ai/claude-agent-sdk";
19
19
 
20
20
  import {
@@ -36,13 +36,9 @@ import { captureBranchChangeset } from "../helpers/git.ts";
36
36
  const MAX_LOOPS = 10;
37
37
 
38
38
  // The orchestrator stage implements the actual code changes and can run for
39
- // a very long time on large tasks. 24 hours prevents premature timeout.
40
- const ORCHESTRATOR_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 hours
41
-
42
- /** Wrap a prompt with a Claude Code @-mention so the named sub-agent runs it. */
43
- function asAgentCall(agentName: string, prompt: string): string {
44
- return `@"${agentName} (agent)" ${prompt}`;
45
- }
39
+ // a very long time on large tasks. Completion is detected via session file
40
+ // watching for idle and result events from Claude's own SDK no manual
41
+ // timeout is needed.
46
42
 
47
43
  /**
48
44
  * Run the Claude Agent SDK's `query()` with structured output and collect
@@ -65,7 +61,7 @@ async function queryWithStructuredOutput(
65
61
  },
66
62
  })) {
67
63
  if (msg.type === "result") {
68
- raw = String((msg as Record<string, unknown>).output ?? "");
64
+ raw = String((msg as Record<string, unknown>).result ?? "");
69
65
  if (
70
66
  msg.subtype === "success" &&
71
67
  (msg as Record<string, unknown>).structured_output
@@ -81,11 +77,15 @@ async function queryWithStructuredOutput(
81
77
  };
82
78
  }
83
79
 
84
- export default defineWorkflow<"claude">({
80
+ export default defineWorkflow({
85
81
  name: "ralph",
86
82
  description:
87
83
  "Plan → orchestrate → review → debug loop with bounded iteration",
84
+ inputs: [
85
+ { name: "prompt", type: "text", required: true, description: "task prompt" },
86
+ ],
88
87
  })
88
+ .for<"claude">()
89
89
  .run(async (ctx) => {
90
90
  const prompt = ctx.inputs.prompt ?? "";
91
91
  let debuggerReport = "";
@@ -94,17 +94,14 @@ export default defineWorkflow<"claude">({
94
94
  // ── Plan ────────────────────────────────────────────────────────────
95
95
  await ctx.stage(
96
96
  { name: `planner-${iteration}` },
97
- {},
97
+ { chatFlags: ["--agent", "planner", "--allow-dangerously-skip-permissions", "--dangerously-skip-permissions"] },
98
98
  {},
99
99
  async (s) => {
100
100
  await s.session.query(
101
- asAgentCall(
102
- "planner",
103
- buildPlannerPrompt(prompt, {
104
- iteration,
105
- debuggerReport: debuggerReport || undefined,
106
- }),
107
- ),
101
+ buildPlannerPrompt(prompt, {
102
+ iteration,
103
+ debuggerReport: debuggerReport || undefined,
104
+ }),
108
105
  );
109
106
  s.save(s.sessionId);
110
107
  },
@@ -113,13 +110,10 @@ export default defineWorkflow<"claude">({
113
110
  // ── Orchestrate ─────────────────────────────────────────────────────
114
111
  await ctx.stage(
115
112
  { name: `orchestrator-${iteration}` },
116
- {},
113
+ { chatFlags: ["--agent", "orchestrator", "--allow-dangerously-skip-permissions", "--dangerously-skip-permissions"] },
117
114
  {},
118
115
  async (s) => {
119
- await s.session.query(
120
- asAgentCall("orchestrator", buildOrchestratorPrompt(prompt)),
121
- { timeoutMs: ORCHESTRATOR_TIMEOUT_MS },
122
- );
116
+ await s.session.query(buildOrchestratorPrompt(prompt));
123
117
  s.save(s.sessionId);
124
118
  },
125
119
  );
@@ -135,10 +129,11 @@ export default defineWorkflow<"claude">({
135
129
  {},
136
130
  async (s) => {
137
131
  const result = await s.session.query(
138
- asAgentCall("codebase-locator", discoveryPrompts.locator),
132
+ discoveryPrompts.locator,
133
+ { agent: "codebase-locator", permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true },
139
134
  );
140
135
  s.save(s.sessionId);
141
- return String(result.output ?? "");
136
+ return extractAssistantText(result, 0);
142
137
  },
143
138
  ),
144
139
  ctx.stage(
@@ -147,10 +142,11 @@ export default defineWorkflow<"claude">({
147
142
  {},
148
143
  async (s) => {
149
144
  const result = await s.session.query(
150
- asAgentCall("codebase-analyzer", discoveryPrompts.analyzer),
145
+ discoveryPrompts.analyzer,
146
+ { agent: "codebase-analyzer", permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true },
151
147
  );
152
148
  s.save(s.sessionId);
153
- return String(result.output ?? "");
149
+ return extractAssistantText(result, 0);
154
150
  },
155
151
  ),
156
152
  ctx.stage(
@@ -159,10 +155,11 @@ export default defineWorkflow<"claude">({
159
155
  {},
160
156
  async (s) => {
161
157
  const result = await s.session.query(
162
- asAgentCall("codebase-pattern-finder", discoveryPrompts.patternFinder),
158
+ discoveryPrompts.patternFinder,
159
+ { agent: "codebase-pattern-finder", permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true },
163
160
  );
164
161
  s.save(s.sessionId);
165
- return String(result.output ?? "");
162
+ return extractAssistantText(result, 0);
166
163
  },
167
164
  ),
168
165
  ]);
@@ -214,20 +211,17 @@ export default defineWorkflow<"claude">({
214
211
  if (iteration < MAX_LOOPS) {
215
212
  const debugger_ = await ctx.stage(
216
213
  { name: `debugger-${iteration}` },
217
- {},
214
+ { chatFlags: ["--agent", "debugger", "--allow-dangerously-skip-permissions", "--dangerously-skip-permissions"] },
218
215
  {},
219
216
  async (s) => {
220
217
  const result = await s.session.query(
221
- asAgentCall(
222
- "debugger",
223
- buildDebuggerReportPrompt(parsed, reviewRaw, {
224
- iteration,
225
- changeset,
226
- }),
227
- ),
218
+ buildDebuggerReportPrompt(parsed, reviewRaw, {
219
+ iteration,
220
+ changeset,
221
+ }),
228
222
  );
229
223
  s.save(s.sessionId);
230
- return result.output;
224
+ return extractAssistantText(result, 0);
231
225
  },
232
226
  );
233
227
 
@@ -74,11 +74,15 @@ function getAssistantText(messages: SessionEvent[]): string {
74
74
  .join("\n\n");
75
75
  }
76
76
 
77
- export default defineWorkflow<"copilot">({
77
+ export default defineWorkflow({
78
78
  name: "ralph",
79
79
  description:
80
80
  "Plan → orchestrate → review → debug loop with bounded iteration",
81
+ inputs: [
82
+ { name: "prompt", type: "text", required: true, description: "task prompt" },
83
+ ],
81
84
  })
85
+ .for<"copilot">()
82
86
  .run(async (ctx) => {
83
87
  const userPromptText = ctx.inputs.prompt ?? "";
84
88
  let debuggerReport = "";
@@ -65,11 +65,15 @@ function extractReview(
65
65
  return { structured: null, raw };
66
66
  }
67
67
 
68
- export default defineWorkflow<"opencode">({
68
+ export default defineWorkflow({
69
69
  name: "ralph",
70
70
  description:
71
71
  "Plan → orchestrate → review → debug loop with bounded iteration",
72
+ inputs: [
73
+ { name: "prompt", type: "text", required: true, description: "task prompt" },
74
+ ],
72
75
  })
76
+ .for<"opencode">()
73
77
  .run(async (ctx) => {
74
78
  const prompt = ctx.inputs.prompt ?? "";
75
79
  let debuggerReport = "";
@@ -44,8 +44,8 @@ export type { SessionPromptResponse as OpenCodePromptResponse } from "@opencode-
44
44
  export type { SessionMessage as ClaudeSessionMessage } from "@anthropic-ai/claude-agent-sdk";
45
45
 
46
46
  // Providers
47
- export { createClaudeSession, claudeQuery, clearClaudeSession, validateClaudeWorkflow } from "../providers/claude.ts";
48
- export type { ClaudeSessionOptions, ClaudeQueryOptions, ClaudeQueryResult } from "../providers/claude.ts";
47
+ export { createClaudeSession, claudeQuery, clearClaudeSession, extractAssistantText, validateClaudeWorkflow } from "../providers/claude.ts";
48
+ export type { ClaudeSessionOptions, ClaudeQueryOptions } from "../providers/claude.ts";
49
49
 
50
50
  export { validateCopilotWorkflow } from "../providers/copilot.ts";
51
51