@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.
- package/README.md +329 -50
- package/dist/commands/cli/session.d.ts +67 -0
- package/dist/commands/cli/session.d.ts.map +1 -0
- package/dist/commands/cli/workflow-status.d.ts +63 -0
- package/dist/commands/cli/workflow-status.d.ts.map +1 -0
- package/dist/sdk/commander.d.ts +74 -0
- package/dist/sdk/commander.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/define-workflow.d.ts +18 -9
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +4 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/management-commands.d.ts +42 -0
- package/dist/sdk/management-commands.d.ts.map +1 -0
- package/dist/sdk/registry.d.ts +27 -0
- package/dist/sdk/registry.d.ts.map +1 -0
- package/dist/sdk/runtime/attached-footer.d.ts +1 -1
- package/dist/sdk/runtime/executor-env.d.ts +20 -0
- package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +61 -10
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +147 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/worker-shared.d.ts +42 -0
- package/dist/sdk/worker-shared.d.ts.map +1 -0
- package/dist/sdk/workflow-cli.d.ts +103 -0
- package/dist/sdk/workflow-cli.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
- package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +5 -5
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/cli.ts +85 -144
- package/src/commands/cli/chat/index.ts +10 -0
- package/src/commands/cli/workflow-command.test.ts +279 -938
- package/src/commands/cli/workflow-inputs.test.ts +41 -11
- package/src/commands/cli/workflow-inputs.ts +47 -12
- package/src/commands/cli/workflow-list.test.ts +234 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow.ts +11 -798
- package/src/scripts/constants.ts +2 -1
- package/src/sdk/commander.ts +161 -0
- package/src/sdk/components/workflow-picker-panel.tsx +78 -258
- package/src/sdk/define-workflow.test.ts +104 -11
- package/src/sdk/define-workflow.ts +47 -11
- package/src/sdk/errors.test.ts +16 -0
- package/src/sdk/index.ts +8 -8
- package/src/sdk/management-commands.ts +151 -0
- package/src/sdk/registry.ts +132 -0
- package/src/sdk/runtime/attached-footer.ts +1 -1
- package/src/sdk/runtime/executor-env.ts +45 -0
- package/src/sdk/runtime/executor.test.ts +37 -0
- package/src/sdk/runtime/executor.ts +147 -68
- package/src/sdk/types.ts +169 -4
- package/src/sdk/worker-shared.test.ts +163 -0
- package/src/sdk/worker-shared.ts +155 -0
- package/src/sdk/workflow-cli.ts +409 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin-registry.ts +23 -0
- package/src/sdk/workflows/index.ts +10 -20
- package/src/services/system/auth.test.ts +63 -1
- package/.agents/skills/workflow-creator/SKILL.md +0 -334
- package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
- package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
- package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
- package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
- package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
- package/.agents/skills/workflow-creator/references/session-config.md +0 -384
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
- package/.agents/skills/workflow-creator/references/user-input.md +0 -234
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
- package/dist/sdk/runtime/discovery.d.ts +0 -132
- package/dist/sdk/runtime/discovery.d.ts.map +0 -1
- package/dist/sdk/runtime/executor-entry.d.ts +0 -11
- package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
- package/dist/sdk/runtime/loader.d.ts +0 -70
- package/dist/sdk/runtime/loader.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/src/commands/cli/workflow.test.ts +0 -317
- package/src/sdk/runtime/discovery.ts +0 -368
- package/src/sdk/runtime/executor-entry.ts +0 -18
- 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
|
|
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
|
|
259
|
+
test("returns a new builder with agent set", () => {
|
|
176
260
|
const builder = defineWorkflow({ name: "test" });
|
|
177
|
-
const narrowed = builder.for
|
|
178
|
-
//
|
|
179
|
-
expect(narrowed
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
240
|
+
* .for("copilot")
|
|
205
241
|
* .run(async (ctx) => {
|
|
206
242
|
* ctx.inputs.greeting; // ✓ string | undefined
|
|
207
243
|
* ctx.inputs.prompt; // ✗ compile error — not declared
|
package/src/sdk/errors.test.ts
CHANGED
|
@@ -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
|
-
//
|
|
38
|
-
export {
|
|
39
|
-
|
|
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
|
-
|
|
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 };
|