@bastani/atomic 0.5.12-4 → 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 -2
  13. package/src/sdk/components/workflow-picker-panel.tsx +15 -21
  14. package/src/sdk/define-workflow.test.ts +58 -0
  15. package/src/sdk/define-workflow.ts +48 -30
  16. package/src/sdk/providers/claude.ts +234 -233
  17. package/src/sdk/runtime/discovery.ts +1 -2
  18. package/src/sdk/runtime/executor.ts +6 -1
  19. package/src/sdk/types.ts +24 -19
  20. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +11 -30
  21. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +7 -4
  22. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +6 -2
  23. package/src/sdk/workflows/builtin/ralph/claude/index.ts +32 -38
  24. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +5 -1
  25. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +5 -1
  26. package/src/sdk/workflows/index.ts +2 -2
  27. package/src/sdk/workflow-inputs.ts +0 -54
@@ -43,11 +43,6 @@ import { useLatest } from "./hooks.ts";
43
43
  import { resolveTheme, type TerminalTheme } from "../runtime/theme.ts";
44
44
  import type { AgentType, WorkflowInput } from "../types.ts";
45
45
  import type { WorkflowWithMetadata } from "../runtime/discovery.ts";
46
- import {
47
- DEFAULT_PROMPT_FIELDS,
48
- isFreeformPromptSchema,
49
- normalizePickerInputs,
50
- } from "../workflow-inputs.ts";
51
46
  import { ErrorBoundary } from "./error-boundary.tsx";
52
47
 
53
48
  // ─── Theme ──────────────────────────────────────
@@ -470,7 +465,7 @@ const Preview = memo(function Preview({
470
465
  wf: WorkflowWithMetadata;
471
466
  }) {
472
467
  const theme = usePickerTheme();
473
- const args = normalizePickerInputs(wf.inputs);
468
+ const args = wf.inputs;
474
469
 
475
470
  return (
476
471
  <box
@@ -504,13 +499,16 @@ const Preview = memo(function Preview({
504
499
  </span>
505
500
  </text>
506
501
 
507
- <box height={2} />
508
-
509
- <SectionLabel label="ARGUMENTS" />
510
- <box height={1} />
511
- {args.map((f) => (
512
- <ArgumentRow key={f.name} field={f} />
513
- ))}
502
+ {args.length > 0 && (
503
+ <>
504
+ <box height={2} />
505
+ <SectionLabel label="ARGUMENTS" />
506
+ <box height={1} />
507
+ {args.map((f) => (
508
+ <ArgumentRow key={f.name} field={f} />
509
+ ))}
510
+ </>
511
+ )}
514
512
  </box>
515
513
  );
516
514
  });
@@ -643,7 +641,7 @@ function EnumContent({
643
641
  selected,
644
642
  focused,
645
643
  }: {
646
- values: string[];
644
+ values: readonly string[];
647
645
  selected: string;
648
646
  focused: boolean;
649
647
  }) {
@@ -782,7 +780,7 @@ function InputPhase({
782
780
  onTextChangeRef: React.RefObject<((value: string) => void) | null>;
783
781
  }) {
784
782
  const theme = usePickerTheme();
785
- const isStructured = !isFreeformPromptSchema(workflow.inputs);
783
+ const isStructured = workflow.inputs.length > 0;
786
784
  const scrollboxRef = useRef<ScrollBoxRenderable>(null);
787
785
  const [scrollTop, setScrollTop] = useState(0);
788
786
 
@@ -1241,9 +1239,8 @@ function usePickerKeyboard(state: PickerKeyboardState): void {
1241
1239
  key.stopPropagation();
1242
1240
  const wf = focusedWfRef.current;
1243
1241
  if (wf) {
1244
- const inputs = normalizePickerInputs(wf.inputs);
1245
1242
  const initial: Record<string, string> = {};
1246
- for (const f of inputs) {
1243
+ for (const f of wf.inputs) {
1247
1244
  initial[f.name] =
1248
1245
  f.default ??
1249
1246
  (f.type === "enum" ? (f.values?.[0] ?? "") : "");
@@ -1357,10 +1354,7 @@ export function WorkflowPicker({
1357
1354
  const focusedWf = entries[clampedEntryIdx]?.workflow;
1358
1355
 
1359
1356
  const currentFields = useMemo<readonly WorkflowInput[]>(
1360
- () =>
1361
- focusedWf
1362
- ? normalizePickerInputs(focusedWf.inputs)
1363
- : DEFAULT_PROMPT_FIELDS,
1357
+ () => focusedWf?.inputs ?? [],
1364
1358
  [focusedWf],
1365
1359
  );
1366
1360
  const currentField = currentFields[focusedFieldIdx];
@@ -170,3 +170,61 @@ describe("WorkflowBuilder.compile()", () => {
170
170
  expect(() => builder.compile()).toThrow("has no run callback");
171
171
  });
172
172
  });
173
+
174
+ describe("WorkflowBuilder.for()", () => {
175
+ test("returns the same builder instance (type-only narrowing)", () => {
176
+ const builder = defineWorkflow({ name: "test" });
177
+ const narrowed = builder.for<"copilot">();
178
+ // Same instance — .for() only changes the TypeScript type, not the value
179
+ expect(narrowed === (builder as unknown)).toBe(true);
180
+ });
181
+
182
+ test("chains with run and compile", () => {
183
+ const def = defineWorkflow({
184
+ name: "test",
185
+ inputs: [{ name: "greeting", type: "string" }],
186
+ })
187
+ .for<"copilot">()
188
+ .run(async () => {})
189
+ .compile();
190
+ expect(def.__brand).toBe("WorkflowDefinition");
191
+ expect(def.inputs[0]?.name).toBe("greeting");
192
+ });
193
+ });
194
+
195
+ describe("typed inputs (compile-time)", () => {
196
+ test("structured inputs restrict ctx.inputs keys", () => {
197
+ // This test validates that the type system correctly narrows
198
+ // ctx.inputs to only declared field names. The assertions below
199
+ // are runtime no-ops — the real check is that tsc compiles this
200
+ // file without errors (or produces errors only where expected).
201
+ defineWorkflow({
202
+ name: "typed-test",
203
+ inputs: [
204
+ { name: "greeting", type: "string", required: true },
205
+ { name: "style", type: "enum", values: ["formal", "casual"] },
206
+ ],
207
+ })
208
+ .for<"copilot">()
209
+ .run(async (ctx) => {
210
+ // Declared keys are valid
211
+ const _g: string | undefined = ctx.inputs.greeting;
212
+ const _s: string | undefined = ctx.inputs.style;
213
+ // Undeclared key — would be a compile error without @ts-expect-error
214
+ // @ts-expect-error — "prompt" is not a declared input
215
+ ctx.inputs.prompt;
216
+ expect(true).toBe(true);
217
+ })
218
+ .compile();
219
+ });
220
+
221
+ test("free-form workflows allow any key", () => {
222
+ defineWorkflow({ name: "freeform-test" })
223
+ .for<"copilot">()
224
+ .run(async (ctx) => {
225
+ const _p: string | undefined = ctx.inputs.prompt;
226
+ expect(true).toBe(true);
227
+ })
228
+ .compile();
229
+ });
230
+ });
@@ -2,7 +2,8 @@
2
2
  * Workflow Builder — defines a workflow with a single `.run()` entry point.
3
3
  *
4
4
  * Usage:
5
- * defineWorkflow<"copilot">({ name: "my-workflow", description: "..." })
5
+ * defineWorkflow({ name: "my-workflow", inputs: [...] })
6
+ * .for<"copilot">()
6
7
  * .run(async (ctx) => {
7
8
  * await ctx.stage({ name: "research" }, {}, {}, async (s) => { ... });
8
9
  * await ctx.stage({ name: "plan" }, {}, {}, async (s) => { ... });
@@ -58,16 +59,42 @@ function validateWorkflowInput(input: WorkflowInput, workflowName: string): void
58
59
  * Chainable workflow builder. Records the run callback,
59
60
  * then .compile() seals it into a WorkflowDefinition.
60
61
  */
61
- export class WorkflowBuilder<A extends AgentType = AgentType> {
62
+ export class WorkflowBuilder<A extends AgentType = AgentType, N extends string = string> {
62
63
  /** @internal Brand for detection across package boundaries */
63
64
  readonly __brand = "WorkflowBuilder" as const;
64
65
  private readonly options: WorkflowOptions;
65
- private runFn: ((ctx: WorkflowContext<A>) => Promise<void>) | null = null;
66
+ private runFn: ((ctx: WorkflowContext<A, N>) => Promise<void>) | null = null;
66
67
 
67
68
  constructor(options: WorkflowOptions) {
68
69
  this.options = options;
69
70
  }
70
71
 
72
+ /**
73
+ * Narrow the agent type for this workflow while preserving typed inputs.
74
+ *
75
+ * Use `.for<"copilot">()` **before** `.run()` instead of passing the
76
+ * agent as a type parameter to `defineWorkflow`. This allows TypeScript
77
+ * to infer input names from the `inputs` array AND narrow the agent
78
+ * type for `stage()` callbacks.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * defineWorkflow({
83
+ * name: "my-workflow",
84
+ * inputs: [{ name: "greeting", type: "string" }],
85
+ * })
86
+ * .for<"copilot">()
87
+ * .run(async (ctx) => {
88
+ * ctx.inputs.greeting; // ✓ typed
89
+ * ctx.inputs.prompt; // ✗ compile error
90
+ * })
91
+ * .compile();
92
+ * ```
93
+ */
94
+ for<B extends AgentType>(): WorkflowBuilder<B, N> {
95
+ return this as unknown as WorkflowBuilder<B, N>;
96
+ }
97
+
71
98
  /**
72
99
  * Set the workflow's entry point.
73
100
  *
@@ -76,7 +103,7 @@ export class WorkflowBuilder<A extends AgentType = AgentType> {
76
103
  * reading completed session outputs. Use native TypeScript control flow
77
104
  * (loops, conditionals, `Promise.all()`) for orchestration.
78
105
  */
79
- run(fn: (ctx: WorkflowContext<A>) => Promise<void>): this {
106
+ run(fn: (ctx: WorkflowContext<A, N>) => Promise<void>): this {
80
107
  if (this.runFn) {
81
108
  throw new Error("run() can only be called once per workflow.");
82
109
  }
@@ -93,7 +120,7 @@ export class WorkflowBuilder<A extends AgentType = AgentType> {
93
120
  * After calling compile(), the returned object is consumed by the
94
121
  * Atomic CLI runtime.
95
122
  */
96
- compile(): WorkflowDefinition<A> {
123
+ compile(): WorkflowDefinition<A, N> {
97
124
  if (!this.runFn) {
98
125
  throw new Error(
99
126
  `Workflow "${this.options.name}" has no run callback. ` +
@@ -133,45 +160,36 @@ export class WorkflowBuilder<A extends AgentType = AgentType> {
133
160
  /**
134
161
  * Entry point for defining a workflow.
135
162
  *
136
- * Pass a type parameter to narrow all context types to a specific agent:
163
+ * Write the `inputs` array inline so TypeScript infers literal field
164
+ * names and enforces them on `ctx.inputs`. Use `.for<Agent>()` to
165
+ * narrow the agent type while keeping typed inputs:
137
166
  *
138
167
  * @example
139
168
  * ```typescript
140
169
  * import { defineWorkflow } from "@bastani/atomic/workflows";
141
170
  *
142
- * export default defineWorkflow<"copilot">({
171
+ * export default defineWorkflow({
143
172
  * name: "hello",
144
173
  * description: "Two-session demo",
174
+ * inputs: [
175
+ * { name: "greeting", type: "string", required: true },
176
+ * ],
145
177
  * })
178
+ * .for<"copilot">()
146
179
  * .run(async (ctx) => {
147
- * const describe = await ctx.stage(
148
- * { name: "describe" },
149
- * {},
150
- * {},
151
- * async (s) => {
152
- * // s.client: CopilotClient, s.session: CopilotSession
153
- * await s.session.send({ prompt: s.inputs.prompt ?? "" });
154
- * s.save(await s.session.getMessages());
155
- * },
156
- * );
157
- * await ctx.stage(
158
- * { name: "summarize" },
159
- * {},
160
- * {},
161
- * async (s) => {
162
- * const research = await s.transcript(describe);
163
- * // ...
164
- * },
165
- * );
180
+ * ctx.inputs.greeting; // string | undefined
181
+ * ctx.inputs.prompt; // compile error — not declared
166
182
  * })
167
183
  * .compile();
168
184
  * ```
169
185
  */
170
- export function defineWorkflow<A extends AgentType = AgentType>(
171
- options: WorkflowOptions,
172
- ): WorkflowBuilder<A> {
186
+ export function defineWorkflow<
187
+ const I extends readonly WorkflowInput[] = readonly WorkflowInput[],
188
+ >(
189
+ options: WorkflowOptions<I>,
190
+ ): WorkflowBuilder<AgentType, I[number]["name"]> {
173
191
  if (!options.name || options.name.trim() === "") {
174
192
  throw new Error("Workflow name is required.");
175
193
  }
176
- return new WorkflowBuilder<A>(options);
194
+ return new WorkflowBuilder<AgentType, I[number]["name"]>(options);
177
195
  }