@bastani/atomic 0.5.34 → 0.6.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
@@ -1,5 +1,5 @@
1
1
  import { test, expect, describe } from "bun:test";
2
- import { defineWorkflow, WorkflowBuilder } from "./define-workflow.ts";
2
+ import { defineWorkflow, WorkflowBuilder, RESERVED_INPUT_NAMES } from "./define-workflow.ts";
3
3
  import type { WorkflowInput } from "./types.ts";
4
4
 
5
5
  describe("defineWorkflow", () => {
@@ -39,6 +39,7 @@ describe("WorkflowBuilder.run()", () => {
39
39
  describe("WorkflowBuilder.compile()", () => {
40
40
  test("produces a WorkflowDefinition with correct brand", () => {
41
41
  const def = defineWorkflow({ name: "test" })
42
+ .for("copilot")
42
43
  .run(async () => {})
43
44
  .compile();
44
45
  expect(def.__brand).toBe("WorkflowDefinition");
@@ -46,6 +47,7 @@ describe("WorkflowBuilder.compile()", () => {
46
47
 
47
48
  test("defaults inputs to an empty array when none are declared", () => {
48
49
  const def = defineWorkflow({ name: "test" })
50
+ .for("copilot")
49
51
  .run(async () => {})
50
52
  .compile();
51
53
  expect(def.inputs).toEqual([]);
@@ -70,6 +72,7 @@ describe("WorkflowBuilder.compile()", () => {
70
72
  },
71
73
  ],
72
74
  })
75
+ .for("opencode")
73
76
  .run(async () => {})
74
77
  .compile();
75
78
  expect(def.inputs).toHaveLength(2);
@@ -83,6 +86,7 @@ describe("WorkflowBuilder.compile()", () => {
83
86
  name: "test",
84
87
  inputs: [{ name: "foo", type: "string" }],
85
88
  })
89
+ .for("claude")
86
90
  .run(async () => {})
87
91
  .compile();
88
92
  expect(() => {
@@ -96,6 +100,7 @@ describe("WorkflowBuilder.compile()", () => {
96
100
  name: "bad",
97
101
  inputs: [{ name: "mode", type: "enum" }],
98
102
  })
103
+ .for("copilot")
99
104
  .run(async () => {})
100
105
  .compile(),
101
106
  ).toThrow("declares no `values`");
@@ -114,6 +119,7 @@ describe("WorkflowBuilder.compile()", () => {
114
119
  },
115
120
  ],
116
121
  })
122
+ .for("copilot")
117
123
  .run(async () => {})
118
124
  .compile(),
119
125
  ).toThrow(/not one of its declared values/);
@@ -125,6 +131,7 @@ describe("WorkflowBuilder.compile()", () => {
125
131
  name: "bad",
126
132
  inputs: [{ name: "1bad", type: "string" }],
127
133
  })
134
+ .for("copilot")
128
135
  .run(async () => {})
129
136
  .compile(),
130
137
  ).toThrow(/invalid/);
@@ -139,21 +146,25 @@ describe("WorkflowBuilder.compile()", () => {
139
146
  { name: "foo", type: "string" },
140
147
  ],
141
148
  })
149
+ .for("copilot")
142
150
  .run(async () => {})
143
151
  .compile(),
144
152
  ).toThrow(/duplicate input name/);
145
153
  });
146
154
 
147
- test("preserves name and description", () => {
155
+ test("preserves name, description, and agent", () => {
148
156
  const def = defineWorkflow({ name: "my-wf", description: "A description" })
157
+ .for("claude")
149
158
  .run(async () => {})
150
159
  .compile();
151
160
  expect(def.name).toBe("my-wf");
152
161
  expect(def.description).toBe("A description");
162
+ expect(def.agent).toBe("claude");
153
163
  });
154
164
 
155
165
  test("defaults description to empty string", () => {
156
166
  const def = defineWorkflow({ name: "test" })
167
+ .for("opencode")
157
168
  .run(async () => {})
158
169
  .compile();
159
170
  expect(def.description).toBe("");
@@ -161,22 +172,104 @@ describe("WorkflowBuilder.compile()", () => {
161
172
 
162
173
  test("stores the run function", () => {
163
174
  const fn = async () => {};
164
- const def = defineWorkflow({ name: "test" }).run(fn).compile();
175
+ const def = defineWorkflow({ name: "test" }).for("copilot").run(fn).compile();
165
176
  expect(def.run).toBe(fn);
166
177
  });
167
178
 
168
179
  test("throws if no run callback was provided", () => {
169
- const builder = defineWorkflow({ name: "test" });
180
+ const builder = defineWorkflow({ name: "test" }).for("copilot");
170
181
  expect(() => builder.compile()).toThrow("has no run callback");
171
182
  });
183
+
184
+ test("throws if .for() was not called before compile()", () => {
185
+ const builder = defineWorkflow({ name: "test" }).run(async () => {});
186
+ expect(() => builder.compile()).toThrow("has no agent");
187
+ });
188
+ });
189
+
190
+ describe("RESERVED_INPUT_NAMES — reserved name validation", () => {
191
+ test("RESERVED_INPUT_NAMES is exported and contains expected names", () => {
192
+ expect(RESERVED_INPUT_NAMES).toContain("name");
193
+ expect(RESERVED_INPUT_NAMES).toContain("agent");
194
+ expect(RESERVED_INPUT_NAMES).toContain("detach");
195
+ expect(RESERVED_INPUT_NAMES).toContain("list");
196
+ expect(RESERVED_INPUT_NAMES).toContain("help");
197
+ expect(RESERVED_INPUT_NAMES).toContain("version");
198
+ });
199
+
200
+ // Each reserved name must be rejected individually.
201
+ for (const reserved of RESERVED_INPUT_NAMES) {
202
+ test(`rejects reserved input name "${reserved}"`, () => {
203
+ expect(() =>
204
+ defineWorkflow({
205
+ name: "bad",
206
+ inputs: [{ name: reserved, type: "string" }],
207
+ })
208
+ .for("copilot")
209
+ .run(async () => {})
210
+ .compile(),
211
+ ).toThrow(reserved);
212
+ });
213
+ }
214
+
215
+ test("error message lists all reserved names", () => {
216
+ let message = "";
217
+ try {
218
+ defineWorkflow({
219
+ name: "bad",
220
+ inputs: [{ name: "name", type: "string" }],
221
+ })
222
+ .for("copilot")
223
+ .run(async () => {})
224
+ .compile();
225
+ } catch (e) {
226
+ message = (e as Error).message;
227
+ }
228
+ for (const reserved of RESERVED_INPUT_NAMES) {
229
+ expect(message).toContain(reserved);
230
+ }
231
+ });
232
+
233
+ test("non-reserved input name passes validation", () => {
234
+ expect(() =>
235
+ defineWorkflow({
236
+ name: "ok",
237
+ inputs: [{ name: "topic", type: "string" }],
238
+ })
239
+ .for("copilot")
240
+ .run(async () => {})
241
+ .compile(),
242
+ ).not.toThrow();
243
+ });
244
+
245
+ test("non-reserved name that is a prefix of a reserved name passes", () => {
246
+ expect(() =>
247
+ defineWorkflow({
248
+ name: "ok",
249
+ inputs: [{ name: "named", type: "string" }],
250
+ })
251
+ .for("copilot")
252
+ .run(async () => {})
253
+ .compile(),
254
+ ).not.toThrow();
255
+ });
172
256
  });
173
257
 
174
258
  describe("WorkflowBuilder.for()", () => {
175
- test("returns the same builder instance (type-only narrowing)", () => {
259
+ test("returns a new builder with agent set", () => {
176
260
  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);
261
+ const narrowed = builder.for("copilot");
262
+ // .for() returns a new builder instance
263
+ expect(narrowed).toBeInstanceOf(WorkflowBuilder);
264
+ expect(narrowed).not.toBe(builder as unknown);
265
+ });
266
+
267
+ test("stores agent on the compiled definition", () => {
268
+ const def = defineWorkflow({ name: "test" })
269
+ .for("copilot")
270
+ .run(async () => {})
271
+ .compile();
272
+ expect(def.agent).toBe("copilot");
180
273
  });
181
274
 
182
275
  test("chains with run and compile", () => {
@@ -184,7 +277,7 @@ describe("WorkflowBuilder.for()", () => {
184
277
  name: "test",
185
278
  inputs: [{ name: "greeting", type: "string" }],
186
279
  })
187
- .for<"copilot">()
280
+ .for("copilot")
188
281
  .run(async () => {})
189
282
  .compile();
190
283
  expect(def.__brand).toBe("WorkflowDefinition");
@@ -205,7 +298,7 @@ describe("typed inputs (compile-time)", () => {
205
298
  { name: "style", type: "enum", values: ["formal", "casual"] },
206
299
  ],
207
300
  })
208
- .for<"copilot">()
301
+ .for("copilot")
209
302
  .run(async (ctx) => {
210
303
  // Declared keys are valid
211
304
  const _g: string | undefined = ctx.inputs.greeting;
@@ -220,7 +313,7 @@ describe("typed inputs (compile-time)", () => {
220
313
 
221
314
  test("free-form workflows allow any key", () => {
222
315
  defineWorkflow({ name: "freeform-test" })
223
- .for<"copilot">()
316
+ .for("copilot")
224
317
  .run(async (ctx) => {
225
318
  const _p: string | undefined = ctx.inputs.prompt;
226
319
  expect(true).toBe(true);
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Usage:
5
5
  * defineWorkflow({ name: "my-workflow", inputs: [...] })
6
- * .for<"copilot">()
6
+ * .for("copilot")
7
7
  * .run(async (ctx) => {
8
8
  * await ctx.stage({ name: "research" }, {}, {}, async (s) => { ... });
9
9
  * await ctx.stage({ name: "plan" }, {}, {}, async (s) => { ... });
@@ -21,6 +21,25 @@ import type {
21
21
 
22
22
  type AnyInputs = readonly WorkflowInput[];
23
23
 
24
+ /**
25
+ * Flag and subcommand names reserved by the worker CLI that cannot be used as
26
+ * workflow input names. The first block (`name` / `agent` / `detach` / `list`
27
+ * / `help` / `version`) collides with Commander's own flags on the root
28
+ * command; the second block (`session` / `status`) collides with the auto-
29
+ * registered management subcommands added by `createWorkflowCli` when
30
+ * `includeManagementCommands` is left at its default (`true`).
31
+ */
32
+ export const RESERVED_INPUT_NAMES = [
33
+ "name",
34
+ "agent",
35
+ "detach",
36
+ "list",
37
+ "help",
38
+ "version",
39
+ "session",
40
+ "status",
41
+ ] as const;
42
+
24
43
  /**
25
44
  * Validate a single declared workflow input, throwing on authoring
26
45
  * mistakes that would otherwise surface as confusing runtime errors
@@ -40,6 +59,12 @@ function validateWorkflowInput(input: WorkflowInput, workflowName: string): void
40
59
  `\`--${input.name}\` CLI flag).`,
41
60
  );
42
61
  }
62
+ if ((RESERVED_INPUT_NAMES as readonly string[]).includes(input.name)) {
63
+ throw new Error(
64
+ `defineWorkflow: input name "${input.name}" is reserved by the worker CLI. ` +
65
+ `Rename it. Reserved names: ${RESERVED_INPUT_NAMES.join(", ")}.`,
66
+ );
67
+ }
43
68
  if (input.type === "enum") {
44
69
  if (!Array.isArray(input.values) || input.values.length === 0) {
45
70
  throw new Error(
@@ -89,6 +114,7 @@ export class WorkflowBuilder<
89
114
  readonly __brand = "WorkflowBuilder" as const;
90
115
  private readonly options: WorkflowOptions<I>;
91
116
  private runFn: ((ctx: WorkflowContext<A, I>) => Promise<void>) | null = null;
117
+ private agentValue: AgentType | null = null;
92
118
 
93
119
  constructor(options: WorkflowOptions<I>) {
94
120
  this.options = options;
@@ -97,10 +123,9 @@ export class WorkflowBuilder<
97
123
  /**
98
124
  * Narrow the agent type for this workflow while preserving typed inputs.
99
125
  *
100
- * Use `.for<"copilot">()` **before** `.run()` instead of passing the
101
- * agent as a type parameter to `defineWorkflow`. This allows TypeScript
102
- * to infer input names from the `inputs` array AND narrow the agent
103
- * type for `stage()` callbacks.
126
+ * Pass the agent as a runtime string argument so the compiled
127
+ * {@link WorkflowDefinition} carries the `agent` field required by
128
+ * the registry.
104
129
  *
105
130
  * @example
106
131
  * ```typescript
@@ -108,7 +133,7 @@ export class WorkflowBuilder<
108
133
  * name: "my-workflow",
109
134
  * inputs: [{ name: "greeting", type: "string" }],
110
135
  * })
111
- * .for<"copilot">()
136
+ * .for("copilot")
112
137
  * .run(async (ctx) => {
113
138
  * ctx.inputs.greeting; // ✓ typed
114
139
  * ctx.inputs.prompt; // ✗ compile error
@@ -116,8 +141,11 @@ export class WorkflowBuilder<
116
141
  * .compile();
117
142
  * ```
118
143
  */
119
- for<B extends AgentType>(): WorkflowBuilder<B, I> {
120
- return this as unknown as WorkflowBuilder<B, I>;
144
+ for<B extends AgentType>(agent: B): WorkflowBuilder<B, I> {
145
+ const next = new WorkflowBuilder<B, I>(this.options as WorkflowOptions<I>);
146
+ next.agentValue = agent;
147
+ next.runFn = this.runFn as ((ctx: WorkflowContext<B, I>) => Promise<void>) | null;
148
+ return next;
121
149
  }
122
150
 
123
151
  /**
@@ -170,11 +198,19 @@ export class WorkflowBuilder<
170
198
  }
171
199
  const inputs = Object.freeze(
172
200
  declaredInputs.map((i) => Object.freeze({ ...i })),
173
- ) as readonly WorkflowInput[];
201
+ ) as unknown as I;
202
+
203
+ if (this.agentValue === null) {
204
+ throw new Error(
205
+ `Workflow "${this.options.name}" has no agent. ` +
206
+ `Call .for("copilot") / .for("opencode") / .for("claude") before .compile().`,
207
+ );
208
+ }
174
209
 
175
210
  return {
176
211
  __brand: "WorkflowDefinition" as const,
177
212
  name: this.options.name,
213
+ agent: this.agentValue as A,
178
214
  description: this.options.description ?? "",
179
215
  inputs,
180
216
  minSDKVersion: this.options.minSDKVersion ?? null,
@@ -187,7 +223,7 @@ export class WorkflowBuilder<
187
223
  * Entry point for defining a workflow.
188
224
  *
189
225
  * Write the `inputs` array inline so TypeScript infers literal field
190
- * names and enforces them on `ctx.inputs`. Use `.for<Agent>()` to
226
+ * names and enforces them on `ctx.inputs`. Use `.for(agent)` to
191
227
  * narrow the agent type while keeping typed inputs:
192
228
  *
193
229
  * @example
@@ -201,7 +237,7 @@ export class WorkflowBuilder<
201
237
  * { name: "greeting", type: "string", required: true },
202
238
  * ],
203
239
  * })
204
- * .for<"copilot">()
240
+ * .for("copilot")
205
241
  * .run(async (ctx) => {
206
242
  * ctx.inputs.greeting; // ✓ string | undefined
207
243
  * ctx.inputs.prompt; // ✗ compile error — not declared
@@ -3,6 +3,7 @@ import {
3
3
  MissingDependencyError,
4
4
  WorkflowNotCompiledError,
5
5
  InvalidWorkflowError,
6
+ IncompatibleSDKError,
6
7
  errorMessage,
7
8
  } from "./errors";
8
9
 
@@ -42,6 +43,21 @@ describe("InvalidWorkflowError", () => {
42
43
  });
43
44
  });
44
45
 
46
+ describe("IncompatibleSDKError", () => {
47
+ test("sets name, versions, and message", () => {
48
+ const err = new IncompatibleSDKError("/tmp/wf.ts", "2.0.0", "1.4.0");
49
+ expect(err).toBeInstanceOf(Error);
50
+ expect(err.name).toBe("IncompatibleSDKError");
51
+ expect(err.path).toBe("/tmp/wf.ts");
52
+ expect(err.requiredVersion).toBe("2.0.0");
53
+ expect(err.currentVersion).toBe("1.4.0");
54
+ expect(err.message).toContain("/tmp/wf.ts");
55
+ expect(err.message).toContain("v2.0.0");
56
+ expect(err.message).toContain("v1.4.0");
57
+ expect(err.message).toContain("Update Atomic");
58
+ });
59
+ });
60
+
45
61
  describe("errorMessage", () => {
46
62
  test("extracts message from Error", () => {
47
63
  expect(errorMessage(new Error("boom"))).toBe("boom");
package/src/sdk/index.ts CHANGED
@@ -34,12 +34,12 @@ export type {
34
34
  // Workflow SDK (also available as atomic/workflows subpath)
35
35
  export { defineWorkflow } from "./define-workflow.ts";
36
36
 
37
- // Workflow discovery and execution
38
- export {
39
- discoverWorkflows,
40
- findWorkflow,
41
- } from "./runtime/discovery.ts";
42
-
43
- export { WorkflowLoader } from "./runtime/loader.ts";
37
+ // Registry
38
+ export type { Registry } from "./registry.ts";
39
+ export { createRegistry } from "./registry.ts";
44
40
 
45
- export { executeWorkflow } from "./runtime/executor.ts";
41
+ // WorkflowCli the factory that drives workflow CLIs. Accepts a lone
42
+ // workflow, an array of workflows, or a Registry for programmatic
43
+ // composition. Ships with the interactive picker out of the box.
44
+ export { createWorkflowCli } from "./workflow-cli.ts";
45
+ export type { WorkflowCli, CreateWorkflowCliOptions } from "./types.ts";
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Session + status management subcommands for workflow CLIs.
3
+ *
4
+ * Shared by the atomic CLI root (`atomic session *`, `atomic workflow status`)
5
+ * and by SDK-built CLIs (`createWorkflowCli(...)`). Factoring this out means a
6
+ * user running `bun run src/claude-worker.ts session list` gets the identical
7
+ * command surface as `atomic session list`, without the SDK embedding its own
8
+ * diverging implementation. All queries go through the shared atomic tmux
9
+ * socket, so sessions spawned by SDK-built CLIs and by `atomic workflow -n …`
10
+ * show up interchangeably.
11
+ *
12
+ * Commander options are declared with the same names, descriptions, and
13
+ * behaviour as the atomic root CLI — keep them in sync when the root CLI
14
+ * grows a new option.
15
+ */
16
+
17
+ import type { Command } from "@commander-js/extra-typings";
18
+ import type { SessionScope } from "../commands/cli/session.ts";
19
+
20
+ /** Commander collect helper: accumulates repeated `-a` values into an array. */
21
+ function collectAgent(value: string, previous: string[]): string[] {
22
+ return [...previous, value];
23
+ }
24
+
25
+ /**
26
+ * Attach the `session` subcommand group (`list` / `connect` / `kill`) to a
27
+ * parent Commander command. Returns the created `session` group so callers
28
+ * can attach additional children if they need to.
29
+ *
30
+ * @param parent The Commander command to mount `session` under.
31
+ * @param scope Which session set the list/kill commands operate on. SDK CLIs
32
+ * typically pass `"workflow"` to scope the picker to
33
+ * `atomic-wf-*` sessions only; the atomic root uses `"all"`.
34
+ */
35
+ export function addSessionSubcommand(
36
+ parent: Command,
37
+ scope: SessionScope = "all",
38
+ ): Command {
39
+ const sessionCmd = parent
40
+ .command("session")
41
+ .description("Manage running tmux sessions on the atomic socket");
42
+
43
+ sessionCmd
44
+ .command("list")
45
+ .description("List running sessions on the atomic tmux socket")
46
+ .option(
47
+ "-a, --agent <name>",
48
+ "Filter by agent backend (claude, copilot, opencode); repeatable",
49
+ collectAgent,
50
+ [] as string[],
51
+ )
52
+ .action(async (localOpts) => {
53
+ const { sessionListCommand } = await import(
54
+ "../commands/cli/session.ts"
55
+ );
56
+ const exitCode = await sessionListCommand(localOpts.agent, scope);
57
+ process.exit(exitCode);
58
+ });
59
+
60
+ sessionCmd
61
+ .command("connect")
62
+ .description("Attach to a running session (interactive picker when no id given)")
63
+ .argument("[session_id]", "Session name to connect to")
64
+ .option(
65
+ "-a, --agent <name>",
66
+ "Filter picker by agent backend (claude, copilot, opencode); repeatable",
67
+ collectAgent,
68
+ [] as string[],
69
+ )
70
+ .action(async (sessionId, localOpts) => {
71
+ if (sessionId) {
72
+ const { sessionConnectCommand } = await import(
73
+ "../commands/cli/session.ts"
74
+ );
75
+ const exitCode = await sessionConnectCommand(sessionId);
76
+ process.exit(exitCode);
77
+ } else {
78
+ const { sessionPickerCommand } = await import(
79
+ "../commands/cli/session.ts"
80
+ );
81
+ const exitCode = await sessionPickerCommand(localOpts.agent, scope);
82
+ process.exit(exitCode);
83
+ }
84
+ });
85
+
86
+ sessionCmd
87
+ .command("kill")
88
+ .description("Kill a running session (omit id to kill all in scope)")
89
+ .argument("[session_id]", "Session name to kill (omit to kill all)")
90
+ .option(
91
+ "-a, --agent <name>",
92
+ "Filter by agent backend (claude, copilot, opencode); repeatable",
93
+ collectAgent,
94
+ [] as string[],
95
+ )
96
+ .option("-y, --yes", "Skip the confirmation prompt (required for agent callers)")
97
+ .action(async (sessionId, localOpts) => {
98
+ const { sessionKillCommand } = await import(
99
+ "../commands/cli/session.ts"
100
+ );
101
+ const exitCode = await sessionKillCommand(
102
+ sessionId,
103
+ localOpts.agent,
104
+ scope,
105
+ undefined,
106
+ { yes: localOpts.yes === true },
107
+ );
108
+ process.exit(exitCode);
109
+ });
110
+
111
+ return sessionCmd;
112
+ }
113
+
114
+ /**
115
+ * Attach a top-level `status` subcommand for querying workflow status.
116
+ * Mirrors `atomic workflow status` — same overall-state contract
117
+ * (`in_progress` / `error` / `completed` / `needs_review`) and same JSON
118
+ * shape. Omit the id to list every running workflow on the socket.
119
+ */
120
+ export function addStatusSubcommand(parent: Command): void {
121
+ parent
122
+ .command("status")
123
+ .description(
124
+ "Query workflow status (in_progress, error, completed, needs_review); omit id to list all",
125
+ )
126
+ .argument("[session_id]", "Workflow tmux session id (omit to list all)")
127
+ .option("--format <format>", "Output format: json | text", "json")
128
+ .action(async (sessionId, localOpts) => {
129
+ const { workflowStatusCommand } = await import(
130
+ "../commands/cli/workflow-status.ts"
131
+ );
132
+ const exitCode = await workflowStatusCommand({
133
+ id: sessionId,
134
+ format: localOpts.format === "text" ? "text" : "json",
135
+ });
136
+ process.exit(exitCode);
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Convenience: attach both `session` and `status` subcommands in the order
142
+ * the SDK defaults use. Called by `createWorkflowCli` when
143
+ * `includeManagementCommands` is `true` (the default).
144
+ */
145
+ export function addManagementCommands(
146
+ parent: Command,
147
+ scope: SessionScope = "workflow",
148
+ ): void {
149
+ addSessionSubcommand(parent, scope);
150
+ addStatusSubcommand(parent);
151
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Workflow Registry — immutable, chainable registry of WorkflowDefinition objects.
3
+ *
4
+ * Key scheme: `${agent}/${name}` — each (agent, name) pair is unique.
5
+ * Registering the same key twice throws — no silent overwrites.
6
+ * `register()` is immutable: returns a new Registry, original is unchanged.
7
+ */
8
+
9
+ import type { AgentType, Registry, RegistrableWorkflow, WorkflowDefinition } from "./types.ts";
10
+ import { validateCopilotWorkflow } from "./providers/copilot.ts";
11
+ import { validateOpenCodeWorkflow } from "./providers/opencode.ts";
12
+ import { validateClaudeWorkflow } from "./providers/claude.ts";
13
+ import type { ValidationWarning } from "./types.ts";
14
+
15
+ // Registry type is declared in types.ts; re-export it from here for convenience.
16
+ export type { Registry };
17
+
18
+ // ─── Validator dispatch ──────────────────────────────────────────────────────
19
+
20
+ /** Map agent type to its provider validator. */
21
+ const providerValidators: Record<
22
+ AgentType,
23
+ (source: string) => ValidationWarning[]
24
+ > = {
25
+ claude: validateClaudeWorkflow,
26
+ opencode: validateOpenCodeWorkflow,
27
+ copilot: validateCopilotWorkflow,
28
+ };
29
+
30
+ /**
31
+ * Run provider-specific source validation for a workflow definition.
32
+ *
33
+ * Derives source text from `wf.run.toString()` — the function body contains
34
+ * the SDK API calls the validators check via regex. Hard failures (thrown
35
+ * errors from the validator itself) propagate; warnings are returned.
36
+ */
37
+ function runProviderValidation(wf: WorkflowDefinition): ValidationWarning[] {
38
+ const validator = providerValidators[wf.agent];
39
+ const source = wf.run.toString();
40
+ return validator(source);
41
+ }
42
+
43
+ /**
44
+ * Validate a single workflow definition at registration time.
45
+ * Throws on hard failures; logs warnings via console.warn with `[registry]` prefix.
46
+ */
47
+ function validateAtRegistration(wf: WorkflowDefinition): void {
48
+ const warnings = runProviderValidation(wf);
49
+ for (const w of warnings) {
50
+ console.warn(
51
+ `[registry] workflow "${wf.agent}/${wf.name}" — ${w.rule}: ${w.message}`,
52
+ );
53
+ }
54
+ }
55
+
56
+ // ─── Implementation ──────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Internal implementation — typed separately from the public `Registry<T>`
60
+ * so the accumulating generic can be rebuilt on each `register()` call
61
+ * without leaking the implementation detail.
62
+ */
63
+ class RegistryImpl<T extends Record<string, WorkflowDefinition>> {
64
+ /** Immutable snapshot of registered definitions, keyed by `${agent}/${name}`. */
65
+ private readonly map: ReadonlyMap<string, WorkflowDefinition>;
66
+
67
+ constructor(map: ReadonlyMap<string, WorkflowDefinition>) {
68
+ this.map = map;
69
+ }
70
+
71
+ register<W extends RegistrableWorkflow>(
72
+ wf: W,
73
+ ): Registry<T & Record<`${W["agent"]}/${W["name"]}`, W>> {
74
+ const key = `${wf.agent}/${wf.name}` as `${W["agent"]}/${W["name"]}`;
75
+
76
+ if (this.map.has(key)) {
77
+ throw new Error(
78
+ `[atomic] Duplicate workflow registration: "${key}" is already registered. ` +
79
+ `Each (agent, name) pair must be unique.`,
80
+ );
81
+ }
82
+
83
+ validateAtRegistration(wf);
84
+
85
+ const next = new Map(this.map);
86
+ next.set(key, wf);
87
+ return new RegistryImpl<T & Record<`${W["agent"]}/${W["name"]}`, W>>(next) as Registry<
88
+ T & Record<`${W["agent"]}/${W["name"]}`, W>
89
+ >;
90
+ }
91
+
92
+ get<K extends keyof T>(key: K): T[K] {
93
+ const entry = this.map.get(key as string);
94
+ if (!entry) {
95
+ throw new Error(`[atomic] Workflow "${String(key)}" is not registered.`);
96
+ }
97
+ return entry as T[K];
98
+ }
99
+
100
+ has(key: string): boolean {
101
+ return this.map.has(key);
102
+ }
103
+
104
+ list(): readonly WorkflowDefinition[] {
105
+ return Object.freeze(Array.from(this.map.values()));
106
+ }
107
+
108
+ resolve(name: string, agent: AgentType): WorkflowDefinition | undefined {
109
+ return this.map.get(`${agent}/${name}`);
110
+ }
111
+ }
112
+
113
+ // ─── Factory ─────────────────────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Create an empty workflow registry.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * import { createRegistry } from "@bastani/atomic/workflows";
121
+ * import { myWorkflow } from "./workflows/my-workflow.workflow";
122
+ *
123
+ * const registry = createRegistry()
124
+ * .register(myWorkflow);
125
+ * ```
126
+ */
127
+ export function createRegistry(): Registry<Record<string, never>> {
128
+ return new RegistryImpl<Record<string, never>>(new Map()) as Registry<Record<string, never>>;
129
+ }
130
+
131
+ // ─── Re-export validators for external use ───────────────────────────────────
132
+ export { validateCopilotWorkflow, validateOpenCodeWorkflow, validateClaudeWorkflow };