@bastani/atomic 0.5.34-0 → 0.6.0-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.
Files changed (94) hide show
  1. package/README.md +329 -50
  2. package/dist/commands/cli/session.d.ts +67 -0
  3. package/dist/commands/cli/session.d.ts.map +1 -0
  4. package/dist/commands/cli/workflow-status.d.ts +63 -0
  5. package/dist/commands/cli/workflow-status.d.ts.map +1 -0
  6. package/dist/sdk/commander.d.ts +74 -0
  7. package/dist/sdk/commander.d.ts.map +1 -0
  8. package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
  9. package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
  10. package/dist/sdk/define-workflow.d.ts +18 -9
  11. package/dist/sdk/define-workflow.d.ts.map +1 -1
  12. package/dist/sdk/index.d.ts +4 -3
  13. package/dist/sdk/index.d.ts.map +1 -1
  14. package/dist/sdk/management-commands.d.ts +42 -0
  15. package/dist/sdk/management-commands.d.ts.map +1 -0
  16. package/dist/sdk/registry.d.ts +27 -0
  17. package/dist/sdk/registry.d.ts.map +1 -0
  18. package/dist/sdk/runtime/attached-footer.d.ts +1 -1
  19. package/dist/sdk/runtime/executor-env.d.ts +20 -0
  20. package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
  21. package/dist/sdk/runtime/executor.d.ts +61 -10
  22. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  23. package/dist/sdk/types.d.ts +147 -4
  24. package/dist/sdk/types.d.ts.map +1 -1
  25. package/dist/sdk/worker-shared.d.ts +42 -0
  26. package/dist/sdk/worker-shared.d.ts.map +1 -0
  27. package/dist/sdk/workflow-cli.d.ts +103 -0
  28. package/dist/sdk/workflow-cli.d.ts.map +1 -0
  29. package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
  30. package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
  31. package/dist/sdk/workflows/index.d.ts +5 -5
  32. package/dist/sdk/workflows/index.d.ts.map +1 -1
  33. package/package.json +12 -8
  34. package/src/cli.ts +85 -144
  35. package/src/commands/cli/chat/index.ts +10 -0
  36. package/src/commands/cli/workflow-command.test.ts +279 -938
  37. package/src/commands/cli/workflow-inputs.test.ts +41 -11
  38. package/src/commands/cli/workflow-inputs.ts +47 -12
  39. package/src/commands/cli/workflow-list.test.ts +234 -0
  40. package/src/commands/cli/workflow-list.ts +0 -0
  41. package/src/commands/cli/workflow.ts +11 -798
  42. package/src/scripts/constants.ts +2 -1
  43. package/src/sdk/commander.ts +161 -0
  44. package/src/sdk/components/workflow-picker-panel.tsx +78 -258
  45. package/src/sdk/define-workflow.test.ts +104 -11
  46. package/src/sdk/define-workflow.ts +47 -11
  47. package/src/sdk/errors.test.ts +16 -0
  48. package/src/sdk/index.ts +8 -8
  49. package/src/sdk/management-commands.ts +151 -0
  50. package/src/sdk/registry.ts +132 -0
  51. package/src/sdk/runtime/attached-footer.ts +1 -1
  52. package/src/sdk/runtime/executor-env.ts +45 -0
  53. package/src/sdk/runtime/executor.test.ts +37 -0
  54. package/src/sdk/runtime/executor.ts +147 -68
  55. package/src/sdk/types.ts +169 -4
  56. package/src/sdk/worker-shared.test.ts +163 -0
  57. package/src/sdk/worker-shared.ts +155 -0
  58. package/src/sdk/workflow-cli.ts +409 -0
  59. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
  60. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
  61. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
  62. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
  63. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
  64. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
  65. package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
  66. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
  67. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
  68. package/src/sdk/workflows/builtin-registry.ts +23 -0
  69. package/src/sdk/workflows/index.ts +10 -20
  70. package/src/services/system/auth.test.ts +63 -1
  71. package/.agents/skills/workflow-creator/SKILL.md +0 -334
  72. package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
  73. package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
  74. package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
  75. package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
  76. package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
  77. package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
  78. package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
  79. package/.agents/skills/workflow-creator/references/session-config.md +0 -384
  80. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
  81. package/.agents/skills/workflow-creator/references/user-input.md +0 -234
  82. package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
  83. package/dist/sdk/runtime/discovery.d.ts +0 -132
  84. package/dist/sdk/runtime/discovery.d.ts.map +0 -1
  85. package/dist/sdk/runtime/executor-entry.d.ts +0 -11
  86. package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
  87. package/dist/sdk/runtime/loader.d.ts +0 -70
  88. package/dist/sdk/runtime/loader.d.ts.map +0 -1
  89. package/dist/version.d.ts +0 -2
  90. package/dist/version.d.ts.map +0 -1
  91. package/src/commands/cli/workflow.test.ts +0 -317
  92. package/src/sdk/runtime/discovery.ts +0 -368
  93. package/src/sdk/runtime/executor-entry.ts +0 -18
  94. package/src/sdk/runtime/loader.ts +0 -267
@@ -0,0 +1,409 @@
1
+ /**
2
+ * WorkflowCli — the single entry-point factory for workflow CLIs.
3
+ *
4
+ * Parses `-n/--name` + `-a/--agent` from argv, exposes a union of flags
5
+ * across every workflow in the registry, opens an interactive picker when
6
+ * agent is given without a name in a TTY, and handles orchestrator
7
+ * re-entry from detached runs.
8
+ *
9
+ * Framework-agnostic: the returned `WorkflowCli` type has no direct
10
+ * Commander dependency. To embed under a parent Commander CLI, use
11
+ * `toCommand(cli)` from `@bastani/atomic/workflows/commander`.
12
+ *
13
+ * Used by the internal `atomic workflow` command. Per-workflow CLI
14
+ * files call `createWorkflowCli(workflow)` — the same factory supports
15
+ * a lone workflow, an array, or a full `Registry`.
16
+ */
17
+
18
+ import { Command } from "@commander-js/extra-typings";
19
+ import type {
20
+ AgentType,
21
+ Registry,
22
+ RegistrableWorkflow,
23
+ WorkflowCli,
24
+ WorkflowDefinition,
25
+ WorkflowInput,
26
+ CreateWorkflowCliOptions,
27
+ } from "./types.ts";
28
+ import {
29
+ executeWorkflow,
30
+ handleOrchestratorReEntry,
31
+ } from "./runtime/executor.ts";
32
+ import { WorkflowPickerPanel } from "./components/workflow-picker-panel.tsx";
33
+ import { createRegistry } from "./registry.ts";
34
+ import {
35
+ toCamelCase,
36
+ validateAndResolve,
37
+ buildInputUnion,
38
+ } from "./worker-shared.ts";
39
+
40
+ // ─── Input normalization ─────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Normalize the three accepted `createWorkflowCli` input shapes into a
44
+ * `Registry`. Detection is structural:
45
+ *
46
+ * - `Registry` has `.register`, `.list`, `.resolve` methods.
47
+ * - Arrays are iterable; loop-register into a fresh registry.
48
+ * - Anything else is treated as a single compiled workflow.
49
+ */
50
+ function normalizeToRegistry<T extends Record<string, WorkflowDefinition>>(
51
+ target: unknown,
52
+ ): Registry<T> {
53
+ // Registry detection — check for the `register` method (distinct from
54
+ // a workflow's `run`, which is the only thing a plain definition has).
55
+ if (target && typeof target === "object" && "register" in target &&
56
+ typeof (target as { register?: unknown }).register === "function") {
57
+ return target as Registry<T>;
58
+ }
59
+
60
+ // Array of workflows — loop-register.
61
+ if (Array.isArray(target)) {
62
+ let reg = createRegistry() as Registry;
63
+ for (const wf of target) {
64
+ reg = reg.register(wf as Parameters<typeof reg.register>[0]);
65
+ }
66
+ return reg as Registry<T>;
67
+ }
68
+
69
+ // Single workflow.
70
+ return createRegistry().register(
71
+ target as Parameters<ReturnType<typeof createRegistry>["register"]>[0],
72
+ ) as Registry<T>;
73
+ }
74
+
75
+ // ─── Constants ───────────────────────────────────────────────────────────────
76
+
77
+ const VALID_AGENTS: readonly AgentType[] = ["claude", "opencode", "copilot"];
78
+
79
+ // ─── Core dispatch (internal — shared with the Commander adapter) ───────────
80
+
81
+ /**
82
+ * Resolve the workflow definition, merge inputs (with precedence), validate,
83
+ * and hand off to the executor.
84
+ *
85
+ * Input precedence (highest → lowest):
86
+ * cliInputs > runInputs > dispatcherInputs > defineWorkflow defaults
87
+ *
88
+ * Exported for `./commander.ts` — not part of the public API.
89
+ */
90
+ export async function resolveAndStart(
91
+ registry: Registry,
92
+ name: string,
93
+ agent: AgentType,
94
+ opts: {
95
+ cliInputs?: Record<string, string>;
96
+ runInputs?: Record<string, string>;
97
+ dispatcherInputs?: Record<string, string>;
98
+ detach?: boolean;
99
+ entry: string;
100
+ },
101
+ ): Promise<void> {
102
+ const def = registry.resolve(name, agent);
103
+ if (!def) {
104
+ const available = registry
105
+ .list()
106
+ .filter((w) => w.name === name)
107
+ .map((w) => w.agent);
108
+ const availableMsg =
109
+ available.length > 0
110
+ ? `available agents for "${name}": ${available.join(", ")}`
111
+ : `no workflow named "${name}" in registry`;
112
+ throw new Error(
113
+ `no workflow named "${name}" for agent "${agent}"; ${availableMsg}`,
114
+ );
115
+ }
116
+
117
+ const merged: Record<string, string> = {
118
+ ...opts.dispatcherInputs,
119
+ ...opts.runInputs,
120
+ ...opts.cliInputs,
121
+ };
122
+
123
+ const resolvedInputs =
124
+ def.inputs.length > 0
125
+ ? validateAndResolve(merged, def.inputs)
126
+ : { ...merged };
127
+
128
+ await executeWorkflow({
129
+ definition: def,
130
+ agent,
131
+ inputs: resolvedInputs,
132
+ entrypointFile: opts.entry,
133
+ workflowKey: `${agent}/${name}`,
134
+ detach: opts.detach ?? false,
135
+ });
136
+ }
137
+
138
+ // ─── Commander command builder (internal — shared with the adapter) ─────────
139
+
140
+ /**
141
+ * Build the Commander Command that drives the workflow CLI. Used by both
142
+ * the standalone `run()` path and the `toCommand` adapter.
143
+ *
144
+ * Exported for `./commander.ts` — not part of the public API.
145
+ */
146
+ export function buildCliCommand(
147
+ registry: Registry,
148
+ unionInputs: Map<string, WorkflowInput>,
149
+ onAction: (params: {
150
+ name: string | undefined;
151
+ agent: AgentType | undefined;
152
+ cliInputs: Record<string, string>;
153
+ detach: boolean;
154
+ }) => Promise<void>,
155
+ mountName?: string,
156
+ ): Command {
157
+ const allWorkflows = registry.list();
158
+ const allNames = [...new Set(allWorkflows.map((w) => w.name))];
159
+
160
+ const cmd = new Command(mountName);
161
+
162
+ // Required so auto-registered subcommands (session/status) can declare
163
+ // their own `-a <agent>` without the parent greedily binding the flag
164
+ // first. Matches what `atomic workflow` does at the top-level.
165
+ cmd.enablePositionalOptions();
166
+
167
+ cmd.option("-n, --name <name>", "Workflow name", (v) => {
168
+ if (allNames.length > 0 && !allNames.includes(v)) {
169
+ throw new Error(
170
+ `[atomic/worker] Unknown workflow name "${v}". Available: ${allNames.join(", ")}.`,
171
+ );
172
+ }
173
+ return v;
174
+ });
175
+
176
+ cmd.option("-a, --agent <agent>", "Agent (claude | opencode | copilot)", (v) => {
177
+ if (!(VALID_AGENTS as string[]).includes(v)) {
178
+ throw new Error(
179
+ `[atomic/worker] Unknown agent "${v}". Valid agents: ${VALID_AGENTS.join(", ")}.`,
180
+ );
181
+ }
182
+ return v as AgentType;
183
+ });
184
+
185
+ for (const [, input] of unionInputs) {
186
+ const desc =
187
+ input.description ??
188
+ (input.type === "enum" ? `one of: ${(input.values ?? []).join(", ")}` : input.type);
189
+ cmd.option(`--${input.name} <value>`, desc);
190
+ }
191
+
192
+ cmd.option("-d, --detach", "Run workflow in background (detach from tmux)");
193
+
194
+ cmd.argument("[prompt...]", "Free-form prompt (joined, stored as inputs.prompt)");
195
+
196
+ cmd.allowUnknownOption(false);
197
+ cmd.allowExcessArguments(true);
198
+
199
+ cmd.action(async function (this: Command) {
200
+ const options = this.opts() as Record<string, string | boolean | undefined>;
201
+ const promptTokens: string[] = this.args;
202
+
203
+ const name = options["name"] as string | undefined;
204
+ const agent = options["agent"] as AgentType | undefined;
205
+ const detach = options["detach"] === true;
206
+
207
+ const cliInputs: Record<string, string> = {};
208
+ for (const [inputName] of unionInputs) {
209
+ const camelKey = toCamelCase(inputName);
210
+ const v = options[camelKey];
211
+ if (typeof v === "string" && v !== "") {
212
+ cliInputs[inputName] = v;
213
+ }
214
+ }
215
+
216
+ const promptStr = promptTokens.join(" ");
217
+ if (promptStr !== "" && name) {
218
+ const def = registry.resolve(name, agent as AgentType);
219
+ if (def && def.inputs.length === 0) {
220
+ cliInputs["prompt"] = promptStr;
221
+ }
222
+ }
223
+
224
+ await onAction({ name, agent, cliInputs, detach });
225
+ });
226
+
227
+ return cmd;
228
+ }
229
+
230
+ /**
231
+ * Interactive-picker path used by both `run()` and the Commander adapter.
232
+ * Depends on `process.stdout.isTTY`; returns without side effects when
233
+ * the user cancels or no terminal is attached.
234
+ *
235
+ * Exported for `./commander.ts` — not part of the public API.
236
+ */
237
+ export async function runPicker(
238
+ registry: Registry,
239
+ agent: AgentType,
240
+ detach: boolean,
241
+ entry: string,
242
+ dispatcherInputs: Record<string, string> | undefined,
243
+ ): Promise<void> {
244
+ const panel = await WorkflowPickerPanel.create({ agent, registry });
245
+ const result = await panel.waitForSelection();
246
+ panel.destroy();
247
+ if (!result) {
248
+ process.stdout.write("No workflow selected.\n");
249
+ return;
250
+ }
251
+ const { workflow: selectedWf, inputs: pickerInputs } = result;
252
+ await resolveAndStart(registry, selectedWf.name, agent, {
253
+ cliInputs: pickerInputs,
254
+ dispatcherInputs,
255
+ detach,
256
+ entry,
257
+ });
258
+ }
259
+
260
+ // ─── Public factory ──────────────────────────────────────────────────────────
261
+
262
+ /**
263
+ * Create a workflow CLI that resolves `--name` + `--agent` from argv and
264
+ * runs the matching workflow from the registry.
265
+ *
266
+ * Accepts three input shapes — pick whichever is cleanest:
267
+ *
268
+ * - **A single workflow.** `createWorkflowCli(workflow).run()`.
269
+ * - **An array of workflows.** `createWorkflowCli([claude, copilot]).run()`.
270
+ * - **A `Registry`.** For programmatic/dynamic composition, or sharing a
271
+ * registry across multiple CLIs. Build with `createRegistry().register(...)`.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * // Single workflow — ~70% of use cases
276
+ * const cli = createWorkflowCli(workflow);
277
+ * await cli.run();
278
+ *
279
+ * // Multi-workflow, multi-agent — by far the most common multi-case
280
+ * await createWorkflowCli([claude, copilot, opencode]).run();
281
+ *
282
+ * // Dynamic composition
283
+ * const registry = workflowFiles.reduce(
284
+ * (r, wf) => r.register(wf),
285
+ * createRegistry(),
286
+ * );
287
+ * await createWorkflowCli(registry).run();
288
+ * ```
289
+ *
290
+ * To embed under a parent Commander CLI:
291
+ *
292
+ * ```ts
293
+ * import { toCommand, runCli } from "@bastani/atomic/workflows/commander";
294
+ * parent.addCommand(toCommand(cli));
295
+ * await runCli(cli, () => parent.parseAsync());
296
+ * ```
297
+ *
298
+ * The single/array overloads use generic constraints (`W extends
299
+ * RegistrableWorkflow`) rather than a plain parameter type. This matters
300
+ * under `--strictFunctionTypes`: a `WorkflowDefinition<"claude", ...>`
301
+ * will not assign to a property-typed parameter because its narrow
302
+ * `run(ctx: WorkflowContext<"claude">)` is contravariant against the
303
+ * broader target. Routing through a generic `W` lets TS check bivariantly
304
+ * via `extends`, which matches how `Registry.register` already accepts
305
+ * the same inputs.
306
+ */
307
+ export function createWorkflowCli<W extends RegistrableWorkflow>(
308
+ target: W,
309
+ options?: CreateWorkflowCliOptions,
310
+ ): WorkflowCli;
311
+ export function createWorkflowCli<W extends RegistrableWorkflow>(
312
+ target: readonly W[],
313
+ options?: CreateWorkflowCliOptions,
314
+ ): WorkflowCli;
315
+ export function createWorkflowCli<T extends Record<string, WorkflowDefinition>>(
316
+ target: Registry<T>,
317
+ options?: CreateWorkflowCliOptions,
318
+ ): WorkflowCli<T>;
319
+ export function createWorkflowCli<T extends Record<string, WorkflowDefinition>>(
320
+ target: RegistrableWorkflow | readonly RegistrableWorkflow[] | Registry<T>,
321
+ options: CreateWorkflowCliOptions = {},
322
+ ): WorkflowCli<T> {
323
+ const registry = normalizeToRegistry<T>(target);
324
+ const defaultInputs = options.inputs;
325
+ const extend = options.extend;
326
+ const entry = options.entry ?? process.argv[1]!;
327
+ const includeManagementCommands = options.includeManagementCommands !== false;
328
+
329
+ // Build input union at construction time — throws on type conflicts.
330
+ const unionInputs = buildInputUnion(registry.list());
331
+
332
+ const cli: WorkflowCli<T> = {
333
+ registry,
334
+ entry,
335
+ defaults: defaultInputs,
336
+
337
+ async run(runOpts = {}): Promise<void> {
338
+ if (await handleOrchestratorReEntry((n, a) => registry.resolve(n, a))) {
339
+ return;
340
+ }
341
+
342
+ const { argv } = runOpts;
343
+
344
+ if (argv === false) {
345
+ // Programmatic: skip Commander entirely.
346
+ if (!runOpts.name || !runOpts.agent) {
347
+ throw new Error(
348
+ "cli.run({ argv: false }) requires both `name` and `agent`",
349
+ );
350
+ }
351
+ await resolveAndStart(registry, runOpts.name, runOpts.agent, {
352
+ runInputs: runOpts.inputs,
353
+ dispatcherInputs: defaultInputs,
354
+ detach: runOpts.detach,
355
+ entry,
356
+ });
357
+ return;
358
+ }
359
+
360
+ // CLI mode — build a fresh command, fold runOpts in as defaults.
361
+ let cmd!: Command;
362
+ cmd = buildCliCommand(
363
+ registry,
364
+ unionInputs,
365
+ async (params) => {
366
+ // Programmatic `name`/`agent`/`detach` layer beneath parsed values.
367
+ const effectiveName = params.name ?? runOpts.name;
368
+ const effectiveAgent = params.agent ?? runOpts.agent;
369
+ const effectiveDetach = params.detach || (runOpts.detach ?? false);
370
+
371
+ // Interactive picker: agent given, name omitted, running in a real terminal.
372
+ if (!effectiveName && effectiveAgent && process.stdout.isTTY) {
373
+ await runPicker(registry, effectiveAgent, effectiveDetach, entry, defaultInputs);
374
+ return;
375
+ }
376
+
377
+ if (!effectiveName || !effectiveAgent) {
378
+ // Commander's `help()` calls `process.exit()` and is typed `never`.
379
+ cmd.help();
380
+ }
381
+
382
+ await resolveAndStart(registry, effectiveName, effectiveAgent, {
383
+ cliInputs: params.cliInputs,
384
+ runInputs: runOpts.inputs,
385
+ dispatcherInputs: defaultInputs,
386
+ detach: effectiveDetach,
387
+ entry,
388
+ });
389
+ },
390
+ );
391
+
392
+ // Auto-register `session` + `status` subcommands so SDK users get the
393
+ // same monitoring surface as the global `atomic` CLI without needing
394
+ // the global binary. Sessions live on the shared atomic tmux socket,
395
+ // so these are pure pass-throughs. Opt out with
396
+ // `createWorkflowCli(wf, { includeManagementCommands: false })`.
397
+ if (includeManagementCommands) {
398
+ const { addManagementCommands } = await import("./management-commands.ts");
399
+ addManagementCommands(cmd, "workflow");
400
+ }
401
+
402
+ if (extend) extend(cmd);
403
+
404
+ await cmd.parseAsync(argv ?? process.argv);
405
+ },
406
+ };
407
+
408
+ return cli;
409
+ }
@@ -106,7 +106,7 @@ export default defineWorkflow({
106
106
  },
107
107
  ],
108
108
  })
109
- .for<"claude">()
109
+ .for("claude")
110
110
  .run(async (ctx) => {
111
111
  const prompt = ctx.inputs.prompt ?? "";
112
112
  const root = getCodebaseRoot();
@@ -82,7 +82,7 @@ export default defineWorkflow({
82
82
  },
83
83
  ],
84
84
  })
85
- .for<"copilot">()
85
+ .for("copilot")
86
86
  .run(async (ctx) => {
87
87
  const prompt = ctx.inputs.prompt ?? "";
88
88
  const root = getCodebaseRoot();
@@ -75,7 +75,7 @@ export default defineWorkflow({
75
75
  },
76
76
  ],
77
77
  })
78
- .for<"opencode">()
78
+ .for("opencode")
79
79
  .run(async (ctx) => {
80
80
  const prompt = ctx.inputs.prompt ?? "";
81
81
  const root = getCodebaseRoot();
@@ -121,7 +121,7 @@ export default defineWorkflow({
121
121
  },
122
122
  ],
123
123
  })
124
- .for<"claude">()
124
+ .for("claude")
125
125
  .run(async (ctx) => {
126
126
  const prompt = ctx.inputs.prompt ?? "";
127
127
  const reference = ctx.inputs.reference ?? "";
@@ -122,7 +122,7 @@ export default defineWorkflow({
122
122
  },
123
123
  ],
124
124
  })
125
- .for<"copilot">()
125
+ .for("copilot")
126
126
  .run(async (ctx) => {
127
127
  const prompt = ctx.inputs.prompt ?? "";
128
128
  const reference = ctx.inputs.reference ?? "";
@@ -117,7 +117,7 @@ export default defineWorkflow({
117
117
  },
118
118
  ],
119
119
  })
120
- .for<"opencode">()
120
+ .for("opencode")
121
121
  .run(async (ctx) => {
122
122
  const prompt = ctx.inputs.prompt ?? "";
123
123
  const reference = ctx.inputs.reference ?? "";
@@ -85,7 +85,7 @@ export default defineWorkflow({
85
85
  },
86
86
  ],
87
87
  })
88
- .for<"claude">()
88
+ .for("claude")
89
89
  .run(async (ctx) => {
90
90
  const prompt = ctx.inputs.prompt ?? "";
91
91
  const maxLoops = ctx.inputs.max_loops ?? DEFAULT_MAX_LOOPS;
@@ -99,7 +99,7 @@ export default defineWorkflow({
99
99
  },
100
100
  ],
101
101
  })
102
- .for<"copilot">()
102
+ .for("copilot")
103
103
  .run(async (ctx) => {
104
104
  const userPromptText = ctx.inputs.prompt ?? "";
105
105
  const maxLoops = ctx.inputs.max_loops ?? DEFAULT_MAX_LOOPS;
@@ -86,7 +86,7 @@ export default defineWorkflow({
86
86
  },
87
87
  ],
88
88
  })
89
- .for<"opencode">()
89
+ .for("opencode")
90
90
  .run(async (ctx) => {
91
91
  const prompt = ctx.inputs.prompt ?? "";
92
92
  const maxLoops = ctx.inputs.max_loops ?? DEFAULT_MAX_LOOPS;
@@ -0,0 +1,23 @@
1
+ import { createRegistry } from "../registry";
2
+
3
+ // ralph
4
+ import ralphClaude from "./builtin/ralph/claude";
5
+ import ralphCopilot from "./builtin/ralph/copilot";
6
+ import ralphOpencode from "./builtin/ralph/opencode";
7
+
8
+ // deep-research-codebase
9
+ import drcClaude from "./builtin/deep-research-codebase/claude";
10
+ import drcCopilot from "./builtin/deep-research-codebase/copilot";
11
+ import drcOpencode from "./builtin/deep-research-codebase/opencode";
12
+
13
+ // open-claude-design
14
+ import ocdClaude from "./builtin/open-claude-design/claude";
15
+ import ocdCopilot from "./builtin/open-claude-design/copilot";
16
+ import ocdOpencode from "./builtin/open-claude-design/opencode";
17
+
18
+ export function createBuiltinRegistry() {
19
+ return createRegistry()
20
+ .register(ralphClaude).register(ralphCopilot).register(ralphOpencode)
21
+ .register(drcClaude).register(drcCopilot).register(drcOpencode)
22
+ .register(ocdClaude).register(ocdCopilot).register(ocdOpencode);
23
+ }
@@ -7,6 +7,16 @@
7
7
  */
8
8
 
9
9
  export { defineWorkflow, WorkflowBuilder } from "../define-workflow.ts";
10
+ export { createRegistry } from "../registry.ts";
11
+ export type { Registry } from "../registry.ts";
12
+
13
+ // WorkflowCli — the single factory that drives workflow CLIs. Accepts a
14
+ // lone workflow, an array of workflows, or a Registry for programmatic
15
+ // composition. Ships with the interactive picker out of the box.
16
+ export { createWorkflowCli } from "../workflow-cli.ts";
17
+ export type { WorkflowCli, CreateWorkflowCliOptions } from "../types.ts";
18
+
19
+ export type { ArgvMode } from "../types.ts";
10
20
 
11
21
  export type {
12
22
  AgentType,
@@ -87,23 +97,3 @@ export {
87
97
  normalizeTmuxLines,
88
98
  } from "../runtime/tmux.ts";
89
99
 
90
- // Runtime — workflow discovery
91
- export {
92
- AGENTS,
93
- discoverWorkflows,
94
- findWorkflow,
95
- loadWorkflowsMetadata,
96
- WORKFLOWS_GITIGNORE,
97
- } from "../runtime/discovery.ts";
98
- export type {
99
- DiscoveredWorkflow,
100
- WorkflowWithMetadata,
101
- WorkflowMetadataStatus,
102
- } from "../runtime/discovery.ts";
103
-
104
- // Runtime — workflow loader pipeline
105
- export { WorkflowLoader } from "../runtime/loader.ts";
106
-
107
- // Runtime — workflow executor
108
- export { executeWorkflow } from "../runtime/executor.ts";
109
- export type { WorkflowRunOptions } from "../runtime/executor.ts";
@@ -111,7 +111,7 @@ afterAll(() => {
111
111
  mock.module("@anthropic-ai/claude-agent-sdk", () => ({ ...actualClaudeSdk }));
112
112
  });
113
113
 
114
- const { checkAgentAuth } = await import("./auth.ts");
114
+ const { checkAgentAuth, printAuthError } = await import("./auth.ts");
115
115
 
116
116
  beforeEach(() => {
117
117
  copilotStart.mockClear();
@@ -226,3 +226,65 @@ describe("checkAgentAuth(opencode)", () => {
226
226
  expect(claudeInit).not.toHaveBeenCalled();
227
227
  });
228
228
  });
229
+
230
+ describe("printAuthError", () => {
231
+ function captureStderr(): {
232
+ output: () => string;
233
+ restore: () => void;
234
+ } {
235
+ const chunks: string[] = [];
236
+ const orig = process.stderr.write;
237
+ process.stderr.write = ((c: string | Uint8Array) => {
238
+ chunks.push(typeof c === "string" ? c : new TextDecoder().decode(c));
239
+ return true;
240
+ }) as typeof process.stderr.write;
241
+ const origErr = console.error;
242
+ console.error = (...args: unknown[]) => {
243
+ chunks.push(args.map((a) => String(a)).join(" ") + "\n");
244
+ };
245
+ return {
246
+ output: () => chunks.join(""),
247
+ restore: () => {
248
+ process.stderr.write = orig;
249
+ console.error = origErr;
250
+ },
251
+ };
252
+ }
253
+
254
+ test("prints the Claude login hint with optional detail line", () => {
255
+ const cap = captureStderr();
256
+ try {
257
+ printAuthError("claude", { loggedIn: false, detail: "token expired" });
258
+ const out = cap.output();
259
+ expect(out).toContain("Not logged in to Claude Code");
260
+ expect(out).toContain("token expired");
261
+ expect(out).toContain("/login");
262
+ } finally {
263
+ cap.restore();
264
+ }
265
+ });
266
+
267
+ test("omits the detail line when no detail is given", () => {
268
+ const cap = captureStderr();
269
+ try {
270
+ printAuthError("copilot", { loggedIn: false });
271
+ const out = cap.output();
272
+ expect(out).toContain("Not logged in to GitHub Copilot CLI");
273
+ expect(out).toContain("`/login`");
274
+ } finally {
275
+ cap.restore();
276
+ }
277
+ });
278
+
279
+ test("prints the OpenCode login hint", () => {
280
+ const cap = captureStderr();
281
+ try {
282
+ printAuthError("opencode", { loggedIn: false });
283
+ const out = cap.output();
284
+ expect(out).toContain("Not logged in to OpenCode");
285
+ expect(out).toContain("opencode auth login");
286
+ } finally {
287
+ cap.restore();
288
+ }
289
+ });
290
+ });