@bastani/atomic 0.6.4 → 0.6.5-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/.agents/skills/create-spec/SKILL.md +6 -3
- package/.agents/skills/tdd/SKILL.md +107 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/workflow-creator/SKILL.md +550 -0
- package/.agents/skills/workflow-creator/references/agent-sessions.md +891 -0
- package/.agents/skills/workflow-creator/references/agent-setup-recipe.md +266 -0
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +201 -0
- package/.agents/skills/workflow-creator/references/control-flow.md +470 -0
- package/.agents/skills/workflow-creator/references/failure-modes.md +1014 -0
- package/.agents/skills/workflow-creator/references/getting-started.md +392 -0
- package/.agents/skills/workflow-creator/references/registry-and-validation.md +141 -0
- package/.agents/skills/workflow-creator/references/running-workflows.md +418 -0
- package/.agents/skills/workflow-creator/references/session-config.md +384 -0
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +356 -0
- package/.agents/skills/workflow-creator/references/user-input.md +234 -0
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +392 -0
- package/.claude/agents/debugger.md +2 -2
- package/.claude/agents/reviewer.md +1 -1
- package/.claude/agents/worker.md +2 -2
- package/.github/agents/debugger.md +1 -1
- package/.github/agents/worker.md +1 -1
- package/.mcp.json +5 -1
- package/.opencode/agents/debugger.md +1 -1
- package/.opencode/agents/worker.md +1 -1
- package/README.md +236 -201
- package/dist/sdk/define-workflow.d.ts +11 -6
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/errors.d.ts +10 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +21 -9
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/primitives/inputs.d.ts +36 -0
- package/dist/sdk/primitives/inputs.d.ts.map +1 -0
- package/dist/sdk/primitives/metadata.d.ts +40 -0
- package/dist/sdk/primitives/metadata.d.ts.map +1 -0
- package/dist/sdk/primitives/run.d.ts +57 -0
- package/dist/sdk/primitives/run.d.ts.map +1 -0
- package/dist/sdk/primitives/sessions.d.ts +128 -0
- package/dist/sdk/primitives/sessions.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +24 -56
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/orchestrator-entry.d.ts +26 -0
- package/dist/sdk/runtime/orchestrator-entry.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +20 -0
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +26 -86
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/index.d.ts +20 -12
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/dist/services/config/additional-instructions.d.ts +1 -1
- package/dist/services/config/additional-instructions.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/cli.ts +39 -56
- package/src/commands/builtin-registry.ts +37 -0
- package/src/commands/cli/chat/index.ts +1 -3
- package/src/{sdk → commands/cli}/management-commands.ts +15 -55
- package/src/commands/cli/session.ts +1 -1
- package/src/commands/cli/workflow-command.test.ts +250 -16
- package/src/commands/cli/workflow-inputs.test.ts +1 -0
- package/src/commands/cli/workflow-inputs.ts +13 -3
- package/src/commands/cli/workflow-list.test.ts +1 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow-status.ts +1 -1
- package/src/commands/cli/workflow.ts +191 -11
- package/src/sdk/define-workflow.test.ts +47 -16
- package/src/sdk/define-workflow.ts +24 -6
- package/src/sdk/errors.test.ts +11 -0
- package/src/sdk/errors.ts +13 -0
- package/src/sdk/index.test.ts +92 -0
- package/src/sdk/index.ts +71 -15
- package/src/sdk/primitives/inputs.ts +48 -0
- package/src/sdk/primitives/metadata.ts +63 -0
- package/src/sdk/primitives/run.ts +81 -0
- package/src/sdk/primitives/sessions.test.ts +594 -0
- package/src/sdk/primitives/sessions.ts +328 -0
- package/src/sdk/runtime/executor.ts +36 -115
- package/src/sdk/runtime/orchestrator-entry.ts +110 -0
- package/src/sdk/runtime/tmux.ts +33 -0
- package/src/sdk/types.ts +26 -91
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -0
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -0
- package/src/sdk/workflows/index.ts +68 -51
- package/src/services/config/additional-instructions.ts +1 -1
- package/.agents/skills/test-driven-development/SKILL.md +0 -371
- package/.agents/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/dist/commands/cli/session.d.ts +0 -67
- package/dist/commands/cli/session.d.ts.map +0 -1
- package/dist/commands/cli/workflow-status.d.ts +0 -63
- package/dist/commands/cli/workflow-status.d.ts.map +0 -1
- package/dist/sdk/commander.d.ts +0 -74
- package/dist/sdk/commander.d.ts.map +0 -1
- package/dist/sdk/management-commands.d.ts +0 -42
- package/dist/sdk/management-commands.d.ts.map +0 -1
- package/dist/sdk/workflow-cli.d.ts +0 -103
- package/dist/sdk/workflow-cli.d.ts.map +0 -1
- package/dist/sdk/workflows/builtin-registry.d.ts +0 -113
- package/dist/sdk/workflows/builtin-registry.d.ts.map +0 -1
- package/src/sdk/commander.ts +0 -161
- package/src/sdk/workflow-cli.ts +0 -409
- package/src/sdk/workflows/builtin-registry.ts +0 -23
|
@@ -1,16 +1,196 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* `atomic workflow` command — built directly on the SDK's primitives.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Produces a Commander `Command` with the same UX as the previous
|
|
5
|
+
* `createWorkflowCli`-driven implementation:
|
|
6
|
+
* - `-n/--name <name>` selects the workflow
|
|
7
|
+
* - `-a/--agent <agent>` selects the agent backend
|
|
8
|
+
* - `-d/--detach` runs in the background
|
|
9
|
+
* - one `--<input>` flag per declared input across the registry (with
|
|
10
|
+
* reserved-name and type-conflict checks via `buildInputUnion`)
|
|
11
|
+
* - `[prompt...]` positional for free-form workflows
|
|
12
|
+
* - interactive picker when `-a` is given without `-n` in a TTY
|
|
13
|
+
*
|
|
14
|
+
* The exported Command is mounted as a subcommand of the root atomic
|
|
15
|
+
* program (see `src/cli.ts`), which then attaches `list`, `inputs`,
|
|
16
|
+
* `status`, and `session` siblings on top of it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Command } from "@commander-js/extra-typings";
|
|
20
|
+
import {
|
|
21
|
+
type AgentType,
|
|
22
|
+
type WorkflowDefinition,
|
|
23
|
+
type WorkflowInput,
|
|
24
|
+
getInputSchema,
|
|
25
|
+
listWorkflows,
|
|
26
|
+
runWorkflow,
|
|
27
|
+
} from "../../sdk/index.ts";
|
|
28
|
+
import { buildInputUnion, toCamelCase } from "../../sdk/worker-shared.ts";
|
|
29
|
+
import { createBuiltinRegistry } from "../builtin-registry.ts";
|
|
30
|
+
import { WorkflowPickerPanel } from "../../sdk/components/workflow-picker-panel.tsx";
|
|
31
|
+
|
|
32
|
+
const VALID_AGENTS: readonly AgentType[] = ["claude", "opencode", "copilot"];
|
|
33
|
+
|
|
34
|
+
/** Resolve a workflow against the builtin registry, throwing with a usable hint. */
|
|
35
|
+
function resolveWorkflow(
|
|
36
|
+
registry: ReturnType<typeof createBuiltinRegistry>,
|
|
37
|
+
name: string,
|
|
38
|
+
agent: AgentType,
|
|
39
|
+
): WorkflowDefinition {
|
|
40
|
+
const def = registry.resolve(name, agent);
|
|
41
|
+
if (def) return def;
|
|
42
|
+
const sameName = listWorkflows(registry)
|
|
43
|
+
.filter((w) => w.name === name)
|
|
44
|
+
.map((w) => w.agent);
|
|
45
|
+
const hint =
|
|
46
|
+
sameName.length > 0
|
|
47
|
+
? `available agents for "${name}": ${sameName.join(", ")}`
|
|
48
|
+
: `no workflow named "${name}" in registry`;
|
|
49
|
+
throw new Error(
|
|
50
|
+
`[atomic/workflow] no workflow named "${name}" for agent "${agent}"; ${hint}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Run a resolved workflow with merged inputs. */
|
|
55
|
+
async function dispatch(
|
|
56
|
+
workflow: WorkflowDefinition,
|
|
57
|
+
cliInputs: Record<string, string>,
|
|
58
|
+
detach: boolean,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
await runWorkflow({
|
|
61
|
+
workflow,
|
|
62
|
+
inputs: cliInputs,
|
|
63
|
+
detach,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Drive the interactive picker. The picker reads its registry directly
|
|
69
|
+
* (filtered by agent) and returns the chosen workflow + populated input
|
|
70
|
+
* map; we then hand off to `runWorkflow`.
|
|
7
71
|
*/
|
|
72
|
+
async function runPicker(
|
|
73
|
+
registry: ReturnType<typeof createBuiltinRegistry>,
|
|
74
|
+
agent: AgentType,
|
|
75
|
+
detach: boolean,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const panel = await WorkflowPickerPanel.create({ agent, registry });
|
|
78
|
+
const result = await panel.waitForSelection();
|
|
79
|
+
panel.destroy();
|
|
80
|
+
if (!result) {
|
|
81
|
+
process.stdout.write("No workflow selected.\n");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await dispatch(result.workflow, result.inputs, detach);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build the workflow command tree. Exported so third-party CLIs (and
|
|
89
|
+
* tests) can reuse the dispatcher with their own registries.
|
|
90
|
+
*
|
|
91
|
+
* @param registry workflow registry to drive the dispatcher; defaults
|
|
92
|
+
* to the atomic CLI's builtin registry.
|
|
93
|
+
*/
|
|
94
|
+
export function buildWorkflowCommand(
|
|
95
|
+
registry: ReturnType<typeof createBuiltinRegistry> = createBuiltinRegistry(),
|
|
96
|
+
): Command {
|
|
97
|
+
const all = listWorkflows(registry);
|
|
98
|
+
const allNames = [...new Set(all.map((w) => w.name))];
|
|
99
|
+
// buildInputUnion enforces the reserved-name and type-conflict checks
|
|
100
|
+
// the SDK previously ran inside createWorkflowCli.
|
|
101
|
+
const unionInputs: Map<string, WorkflowInput> = buildInputUnion(all);
|
|
102
|
+
|
|
103
|
+
const cmd = new Command("workflow");
|
|
104
|
+
|
|
105
|
+
// Subcommands declare their own `-a`; without enablePositionalOptions
|
|
106
|
+
// the parent would greedily bind the flag.
|
|
107
|
+
cmd.enablePositionalOptions();
|
|
108
|
+
|
|
109
|
+
cmd.option("-n, --name <name>", "Workflow name", (v) => {
|
|
110
|
+
if (allNames.length > 0 && !allNames.includes(v)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`[atomic/workflow] Unknown workflow name "${v}". ` +
|
|
113
|
+
`Available: ${allNames.join(", ")}.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return v;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
cmd.option(
|
|
120
|
+
"-a, --agent <agent>",
|
|
121
|
+
"Agent (claude | opencode | copilot)",
|
|
122
|
+
(v) => {
|
|
123
|
+
if (!(VALID_AGENTS as readonly string[]).includes(v)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`[atomic/workflow] Unknown agent "${v}". ` +
|
|
126
|
+
`Valid agents: ${VALID_AGENTS.join(", ")}.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return v as AgentType;
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
for (const [, input] of unionInputs) {
|
|
134
|
+
const desc =
|
|
135
|
+
input.description ??
|
|
136
|
+
(input.type === "enum"
|
|
137
|
+
? `one of: ${(input.values ?? []).join(", ")}`
|
|
138
|
+
: input.type);
|
|
139
|
+
cmd.option(`--${input.name} <value>`, desc);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
cmd.option("-d, --detach", "Run workflow in background (detach from tmux)");
|
|
143
|
+
|
|
144
|
+
cmd.argument("[prompt...]", "Free-form prompt (joined, stored as inputs.prompt)");
|
|
145
|
+
|
|
146
|
+
cmd.allowUnknownOption(false);
|
|
147
|
+
cmd.allowExcessArguments(true);
|
|
148
|
+
|
|
149
|
+
cmd.action(async function (this: Command) {
|
|
150
|
+
const options = this.opts() as Record<string, string | boolean | undefined>;
|
|
151
|
+
const promptTokens: string[] = this.args;
|
|
152
|
+
|
|
153
|
+
const name = options["name"] as string | undefined;
|
|
154
|
+
const agent = options["agent"] as AgentType | undefined;
|
|
155
|
+
const detach = options["detach"] === true;
|
|
156
|
+
|
|
157
|
+
const cliInputs: Record<string, string> = {};
|
|
158
|
+
for (const [inputName] of unionInputs) {
|
|
159
|
+
const camelKey = toCamelCase(inputName);
|
|
160
|
+
const v = options[camelKey];
|
|
161
|
+
if (typeof v === "string" && v !== "") {
|
|
162
|
+
cliInputs[inputName] = v;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Free-form workflows: collapse the trailing positional args into
|
|
167
|
+
// `inputs.prompt` so workflow authors can keep reading
|
|
168
|
+
// `ctx.inputs.prompt` regardless of declared schema.
|
|
169
|
+
const promptStr = promptTokens.join(" ");
|
|
170
|
+
if (promptStr !== "" && name) {
|
|
171
|
+
const def = registry.resolve(name, agent as AgentType);
|
|
172
|
+
if (def && getInputSchema(def).length === 0) {
|
|
173
|
+
cliInputs["prompt"] = promptStr;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!name && agent && process.stdout.isTTY) {
|
|
178
|
+
await runPicker(registry, agent, detach);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (name === undefined || agent === undefined) {
|
|
183
|
+
// help() exits the process; the explicit `return` keeps narrowing
|
|
184
|
+
// happy for the lines below.
|
|
185
|
+
cmd.help();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const workflow = resolveWorkflow(registry, name, agent);
|
|
190
|
+
await dispatch(workflow, cliInputs, detach);
|
|
191
|
+
});
|
|
8
192
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { createBuiltinRegistry } from "../../sdk/workflows/builtin-registry.ts";
|
|
193
|
+
return cmd;
|
|
194
|
+
}
|
|
12
195
|
|
|
13
|
-
export const workflowCommand =
|
|
14
|
-
createWorkflowCli(createBuiltinRegistry()),
|
|
15
|
-
"workflow",
|
|
16
|
-
);
|
|
196
|
+
export const workflowCommand = buildWorkflowCommand();
|
|
@@ -4,41 +4,60 @@ import type { WorkflowInput } from "./types.ts";
|
|
|
4
4
|
|
|
5
5
|
describe("defineWorkflow", () => {
|
|
6
6
|
test("returns a WorkflowBuilder", () => {
|
|
7
|
-
const builder = defineWorkflow({ name: "test" });
|
|
7
|
+
const builder = defineWorkflow({ name: "test", source: import.meta.path });
|
|
8
8
|
expect(builder).toBeInstanceOf(WorkflowBuilder);
|
|
9
9
|
expect(builder.__brand).toBe("WorkflowBuilder");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
test("throws on empty name", () => {
|
|
13
|
-
expect(() => defineWorkflow({ name: "" })).toThrow("Workflow name is required");
|
|
13
|
+
expect(() => defineWorkflow({ name: "", source: import.meta.path })).toThrow("Workflow name is required");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
test("throws on whitespace-only name", () => {
|
|
17
|
-
expect(() => defineWorkflow({ name: " " })).toThrow("Workflow name is required");
|
|
17
|
+
expect(() => defineWorkflow({ name: " ", source: import.meta.path })).toThrow("Workflow name is required");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("throws on missing source at compile()", () => {
|
|
21
|
+
expect(() =>
|
|
22
|
+
// Cast required because the type requires `source`; this exercises the
|
|
23
|
+
// runtime guard for users who silence the type error.
|
|
24
|
+
defineWorkflow({ name: "no-source" } as unknown as { name: string; source: string })
|
|
25
|
+
.for("copilot")
|
|
26
|
+
.run(async () => {})
|
|
27
|
+
.compile(),
|
|
28
|
+
).toThrow(/missing the `source` option/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("propagates source onto the compiled definition", () => {
|
|
32
|
+
const def = defineWorkflow({ name: "with-src", source: import.meta.path })
|
|
33
|
+
.for("copilot")
|
|
34
|
+
.run(async () => {})
|
|
35
|
+
.compile();
|
|
36
|
+
expect(def.source).toBe(import.meta.path);
|
|
18
37
|
});
|
|
19
38
|
});
|
|
20
39
|
|
|
21
40
|
describe("WorkflowBuilder.run()", () => {
|
|
22
41
|
test("accepts a function and returns this for chaining", () => {
|
|
23
|
-
const builder = defineWorkflow({ name: "test" });
|
|
42
|
+
const builder = defineWorkflow({ name: "test", source: import.meta.path });
|
|
24
43
|
const result = builder.run(async () => {});
|
|
25
44
|
expect(result).toBe(builder);
|
|
26
45
|
});
|
|
27
46
|
|
|
28
47
|
test("throws if called twice", () => {
|
|
29
|
-
const builder = defineWorkflow({ name: "test" }).run(async () => {});
|
|
48
|
+
const builder = defineWorkflow({ name: "test", source: import.meta.path }).run(async () => {});
|
|
30
49
|
expect(() => builder.run(async () => {})).toThrow("run() can only be called once");
|
|
31
50
|
});
|
|
32
51
|
|
|
33
52
|
test("throws if argument is not a function", () => {
|
|
34
|
-
const builder = defineWorkflow({ name: "test" });
|
|
53
|
+
const builder = defineWorkflow({ name: "test", source: import.meta.path });
|
|
35
54
|
expect(() => builder.run("not a function" as never)).toThrow("run() requires a function");
|
|
36
55
|
});
|
|
37
56
|
});
|
|
38
57
|
|
|
39
58
|
describe("WorkflowBuilder.compile()", () => {
|
|
40
59
|
test("produces a WorkflowDefinition with correct brand", () => {
|
|
41
|
-
const def = defineWorkflow({ name: "test" })
|
|
60
|
+
const def = defineWorkflow({ name: "test", source: import.meta.path })
|
|
42
61
|
.for("copilot")
|
|
43
62
|
.run(async () => {})
|
|
44
63
|
.compile();
|
|
@@ -46,7 +65,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
46
65
|
});
|
|
47
66
|
|
|
48
67
|
test("defaults inputs to an empty array when none are declared", () => {
|
|
49
|
-
const def = defineWorkflow({ name: "test" })
|
|
68
|
+
const def = defineWorkflow({ name: "test", source: import.meta.path })
|
|
50
69
|
.for("copilot")
|
|
51
70
|
.run(async () => {})
|
|
52
71
|
.compile();
|
|
@@ -56,6 +75,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
56
75
|
test("preserves declared inputs in order", () => {
|
|
57
76
|
const def = defineWorkflow({
|
|
58
77
|
name: "gen-spec",
|
|
78
|
+
source: import.meta.path,
|
|
59
79
|
inputs: [
|
|
60
80
|
{
|
|
61
81
|
name: "research_doc",
|
|
@@ -84,6 +104,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
84
104
|
test("freezes declared inputs to prevent downstream mutation", () => {
|
|
85
105
|
const def = defineWorkflow({
|
|
86
106
|
name: "test",
|
|
107
|
+
source: import.meta.path,
|
|
87
108
|
inputs: [{ name: "foo", type: "string" }],
|
|
88
109
|
})
|
|
89
110
|
.for("claude")
|
|
@@ -98,6 +119,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
98
119
|
expect(() =>
|
|
99
120
|
defineWorkflow({
|
|
100
121
|
name: "bad",
|
|
122
|
+
source: import.meta.path,
|
|
101
123
|
inputs: [{ name: "mode", type: "enum" }],
|
|
102
124
|
})
|
|
103
125
|
.for("copilot")
|
|
@@ -110,6 +132,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
110
132
|
expect(() =>
|
|
111
133
|
defineWorkflow({
|
|
112
134
|
name: "bad",
|
|
135
|
+
source: import.meta.path,
|
|
113
136
|
inputs: [
|
|
114
137
|
{
|
|
115
138
|
name: "mode",
|
|
@@ -129,6 +152,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
129
152
|
expect(() =>
|
|
130
153
|
defineWorkflow({
|
|
131
154
|
name: "bad",
|
|
155
|
+
source: import.meta.path,
|
|
132
156
|
inputs: [{ name: "1bad", type: "string" }],
|
|
133
157
|
})
|
|
134
158
|
.for("copilot")
|
|
@@ -141,6 +165,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
141
165
|
expect(() =>
|
|
142
166
|
defineWorkflow({
|
|
143
167
|
name: "bad",
|
|
168
|
+
source: import.meta.path,
|
|
144
169
|
inputs: [
|
|
145
170
|
{ name: "foo", type: "string" },
|
|
146
171
|
{ name: "foo", type: "string" },
|
|
@@ -153,7 +178,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
153
178
|
});
|
|
154
179
|
|
|
155
180
|
test("preserves name, description, and agent", () => {
|
|
156
|
-
const def = defineWorkflow({ name: "my-wf", description: "A description" })
|
|
181
|
+
const def = defineWorkflow({ name: "my-wf", description: "A description", source: import.meta.path })
|
|
157
182
|
.for("claude")
|
|
158
183
|
.run(async () => {})
|
|
159
184
|
.compile();
|
|
@@ -163,7 +188,7 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
163
188
|
});
|
|
164
189
|
|
|
165
190
|
test("defaults description to empty string", () => {
|
|
166
|
-
const def = defineWorkflow({ name: "test" })
|
|
191
|
+
const def = defineWorkflow({ name: "test", source: import.meta.path })
|
|
167
192
|
.for("opencode")
|
|
168
193
|
.run(async () => {})
|
|
169
194
|
.compile();
|
|
@@ -172,17 +197,17 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
172
197
|
|
|
173
198
|
test("stores the run function", () => {
|
|
174
199
|
const fn = async () => {};
|
|
175
|
-
const def = defineWorkflow({ name: "test" }).for("copilot").run(fn).compile();
|
|
200
|
+
const def = defineWorkflow({ name: "test", source: import.meta.path }).for("copilot").run(fn).compile();
|
|
176
201
|
expect(def.run).toBe(fn);
|
|
177
202
|
});
|
|
178
203
|
|
|
179
204
|
test("throws if no run callback was provided", () => {
|
|
180
|
-
const builder = defineWorkflow({ name: "test" }).for("copilot");
|
|
205
|
+
const builder = defineWorkflow({ name: "test", source: import.meta.path }).for("copilot");
|
|
181
206
|
expect(() => builder.compile()).toThrow("has no run callback");
|
|
182
207
|
});
|
|
183
208
|
|
|
184
209
|
test("throws if .for() was not called before compile()", () => {
|
|
185
|
-
const builder = defineWorkflow({ name: "test" }).run(async () => {});
|
|
210
|
+
const builder = defineWorkflow({ name: "test", source: import.meta.path }).run(async () => {});
|
|
186
211
|
expect(() => builder.compile()).toThrow("has no agent");
|
|
187
212
|
});
|
|
188
213
|
});
|
|
@@ -203,6 +228,7 @@ describe("RESERVED_INPUT_NAMES — reserved name validation", () => {
|
|
|
203
228
|
expect(() =>
|
|
204
229
|
defineWorkflow({
|
|
205
230
|
name: "bad",
|
|
231
|
+
source: import.meta.path,
|
|
206
232
|
inputs: [{ name: reserved, type: "string" }],
|
|
207
233
|
})
|
|
208
234
|
.for("copilot")
|
|
@@ -217,6 +243,7 @@ describe("RESERVED_INPUT_NAMES — reserved name validation", () => {
|
|
|
217
243
|
try {
|
|
218
244
|
defineWorkflow({
|
|
219
245
|
name: "bad",
|
|
246
|
+
source: import.meta.path,
|
|
220
247
|
inputs: [{ name: "name", type: "string" }],
|
|
221
248
|
})
|
|
222
249
|
.for("copilot")
|
|
@@ -234,6 +261,7 @@ describe("RESERVED_INPUT_NAMES — reserved name validation", () => {
|
|
|
234
261
|
expect(() =>
|
|
235
262
|
defineWorkflow({
|
|
236
263
|
name: "ok",
|
|
264
|
+
source: import.meta.path,
|
|
237
265
|
inputs: [{ name: "topic", type: "string" }],
|
|
238
266
|
})
|
|
239
267
|
.for("copilot")
|
|
@@ -246,6 +274,7 @@ describe("RESERVED_INPUT_NAMES — reserved name validation", () => {
|
|
|
246
274
|
expect(() =>
|
|
247
275
|
defineWorkflow({
|
|
248
276
|
name: "ok",
|
|
277
|
+
source: import.meta.path,
|
|
249
278
|
inputs: [{ name: "named", type: "string" }],
|
|
250
279
|
})
|
|
251
280
|
.for("copilot")
|
|
@@ -257,7 +286,7 @@ describe("RESERVED_INPUT_NAMES — reserved name validation", () => {
|
|
|
257
286
|
|
|
258
287
|
describe("WorkflowBuilder.for()", () => {
|
|
259
288
|
test("returns a new builder with agent set", () => {
|
|
260
|
-
const builder = defineWorkflow({ name: "test" });
|
|
289
|
+
const builder = defineWorkflow({ name: "test", source: import.meta.path });
|
|
261
290
|
const narrowed = builder.for("copilot");
|
|
262
291
|
// .for() returns a new builder instance
|
|
263
292
|
expect(narrowed).toBeInstanceOf(WorkflowBuilder);
|
|
@@ -265,7 +294,7 @@ describe("WorkflowBuilder.for()", () => {
|
|
|
265
294
|
});
|
|
266
295
|
|
|
267
296
|
test("stores agent on the compiled definition", () => {
|
|
268
|
-
const def = defineWorkflow({ name: "test" })
|
|
297
|
+
const def = defineWorkflow({ name: "test", source: import.meta.path })
|
|
269
298
|
.for("copilot")
|
|
270
299
|
.run(async () => {})
|
|
271
300
|
.compile();
|
|
@@ -275,6 +304,7 @@ describe("WorkflowBuilder.for()", () => {
|
|
|
275
304
|
test("chains with run and compile", () => {
|
|
276
305
|
const def = defineWorkflow({
|
|
277
306
|
name: "test",
|
|
307
|
+
source: import.meta.path,
|
|
278
308
|
inputs: [{ name: "greeting", type: "string" }],
|
|
279
309
|
})
|
|
280
310
|
.for("copilot")
|
|
@@ -293,6 +323,7 @@ describe("typed inputs (compile-time)", () => {
|
|
|
293
323
|
// file without errors (or produces errors only where expected).
|
|
294
324
|
defineWorkflow({
|
|
295
325
|
name: "typed-test",
|
|
326
|
+
source: import.meta.path,
|
|
296
327
|
inputs: [
|
|
297
328
|
{ name: "greeting", type: "string", required: true },
|
|
298
329
|
{ name: "style", type: "enum", values: ["formal", "casual"] },
|
|
@@ -312,7 +343,7 @@ describe("typed inputs (compile-time)", () => {
|
|
|
312
343
|
});
|
|
313
344
|
|
|
314
345
|
test("free-form workflows allow any key", () => {
|
|
315
|
-
defineWorkflow({ name: "freeform-test" })
|
|
346
|
+
defineWorkflow({ name: "freeform-test", source: import.meta.path })
|
|
316
347
|
.for("copilot")
|
|
317
348
|
.run(async (ctx) => {
|
|
318
349
|
const _p: string | undefined = ctx.inputs.prompt;
|
|
@@ -22,12 +22,17 @@ import type {
|
|
|
22
22
|
type AnyInputs = readonly WorkflowInput[];
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* / `help` / `version`) collides with
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* Input names reserved because they collide with the atomic CLI's `workflow`
|
|
26
|
+
* subcommand surface. The first block (`name` / `agent` / `detach` / `list`
|
|
27
|
+
* / `help` / `version`) collides with the atomic CLI's `workflow` subcommand
|
|
28
|
+
* flags (`-n/--name`, `-a/--agent`, `-d/--detach`, `-l/--list`, `-h/--help`,
|
|
29
|
+
* `-v/--version`). The second block (`session` / `status`) collides with the
|
|
30
|
+
* atomic CLI's management subcommands (`atomic workflow session …`,
|
|
31
|
+
* `atomic workflow status`).
|
|
32
|
+
*
|
|
33
|
+
* User-app CLIs built with the SDK primitives are NOT bound by these
|
|
34
|
+
* reservations at runtime — the check runs only inside `defineWorkflow` so
|
|
35
|
+
* that workflows remain portable to the atomic CLI without renaming.
|
|
31
36
|
*/
|
|
32
37
|
export const RESERVED_INPUT_NAMES = [
|
|
33
38
|
"name",
|
|
@@ -207,6 +212,18 @@ export class WorkflowBuilder<
|
|
|
207
212
|
);
|
|
208
213
|
}
|
|
209
214
|
|
|
215
|
+
if (
|
|
216
|
+
typeof this.options.source !== "string" ||
|
|
217
|
+
this.options.source.trim() === ""
|
|
218
|
+
) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Workflow "${this.options.name}" is missing the \`source\` option. ` +
|
|
221
|
+
`Pass \`source: import.meta.path\` in the \`defineWorkflow({ ... })\` ` +
|
|
222
|
+
`call so the SDK orchestrator can re-import the workflow module ` +
|
|
223
|
+
`inside the spawned tmux session.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
return {
|
|
211
228
|
__brand: "WorkflowDefinition" as const,
|
|
212
229
|
name: this.options.name,
|
|
@@ -214,6 +231,7 @@ export class WorkflowBuilder<
|
|
|
214
231
|
description: this.options.description ?? "",
|
|
215
232
|
inputs,
|
|
216
233
|
minSDKVersion: this.options.minSDKVersion ?? null,
|
|
234
|
+
source: this.options.source,
|
|
217
235
|
run: runFn,
|
|
218
236
|
};
|
|
219
237
|
}
|
package/src/sdk/errors.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
WorkflowNotCompiledError,
|
|
5
5
|
InvalidWorkflowError,
|
|
6
6
|
IncompatibleSDKError,
|
|
7
|
+
SessionNotFoundError,
|
|
7
8
|
errorMessage,
|
|
8
9
|
} from "./errors";
|
|
9
10
|
|
|
@@ -43,6 +44,16 @@ describe("InvalidWorkflowError", () => {
|
|
|
43
44
|
});
|
|
44
45
|
});
|
|
45
46
|
|
|
47
|
+
describe("SessionNotFoundError", () => {
|
|
48
|
+
test("sets name, id, and message", () => {
|
|
49
|
+
const err = new SessionNotFoundError("atomic-wf-claude-ralph-deadbeef");
|
|
50
|
+
expect(err).toBeInstanceOf(Error);
|
|
51
|
+
expect(err.name).toBe("SessionNotFoundError");
|
|
52
|
+
expect(err.id).toBe("atomic-wf-claude-ralph-deadbeef");
|
|
53
|
+
expect(err.message).toBe("session not found: atomic-wf-claude-ralph-deadbeef");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
46
57
|
describe("IncompatibleSDKError", () => {
|
|
47
58
|
test("sets name, versions, and message", () => {
|
|
48
59
|
const err = new IncompatibleSDKError("/tmp/wf.ts", "2.0.0", "1.4.0");
|
package/src/sdk/errors.ts
CHANGED
|
@@ -38,6 +38,19 @@ export class InvalidWorkflowError extends Error {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Thrown by session primitives when the requested tmux session id is
|
|
43
|
+
* not present on the atomic socket. Carries the id so the CLI layer can
|
|
44
|
+
* render an actionable "run `atomic session list` to see what's
|
|
45
|
+
* running" hint without parsing message text.
|
|
46
|
+
*/
|
|
47
|
+
export class SessionNotFoundError extends Error {
|
|
48
|
+
constructor(public readonly id: string) {
|
|
49
|
+
super(`session not found: ${id}`);
|
|
50
|
+
this.name = "SessionNotFoundError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/**
|
|
42
55
|
* Thrown when a workflow declares a `minSDKVersion` newer than the
|
|
43
56
|
* bundled CLI. Carries both versions so the CLI can render an
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the registry-iteration helpers exported from the SDK barrel.
|
|
3
|
+
*
|
|
4
|
+
* `listWorkflows` and `getWorkflow` are thin wrappers around the
|
|
5
|
+
* registry's `list` / `resolve` methods, but they're the public surface
|
|
6
|
+
* that downstream CLIs use to enumerate registered workflows — worth
|
|
7
|
+
* pinning to a regression test so changes to the wrapper signature
|
|
8
|
+
* surface here first.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from "bun:test";
|
|
12
|
+
import {
|
|
13
|
+
createRegistry,
|
|
14
|
+
defineWorkflow,
|
|
15
|
+
getWorkflow,
|
|
16
|
+
listWorkflows,
|
|
17
|
+
} from "./index.ts";
|
|
18
|
+
|
|
19
|
+
function makeWorkflow(name: string, agent: "claude" | "copilot" | "opencode") {
|
|
20
|
+
return defineWorkflow({ name, source: import.meta.path })
|
|
21
|
+
.for(agent)
|
|
22
|
+
.run(async () => {})
|
|
23
|
+
.compile();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("listWorkflows", () => {
|
|
27
|
+
test("returns an empty array for an empty registry", () => {
|
|
28
|
+
const registry = createRegistry();
|
|
29
|
+
expect(listWorkflows(registry)).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns every registered workflow regardless of agent", () => {
|
|
33
|
+
const registry = createRegistry()
|
|
34
|
+
.register(makeWorkflow("a", "claude"))
|
|
35
|
+
.register(makeWorkflow("b", "copilot"))
|
|
36
|
+
.register(makeWorkflow("c", "opencode"));
|
|
37
|
+
|
|
38
|
+
const all = listWorkflows(registry);
|
|
39
|
+
expect(all).toHaveLength(3);
|
|
40
|
+
expect(all.map((w) => `${w.agent}/${w.name}`).sort()).toEqual([
|
|
41
|
+
"claude/a",
|
|
42
|
+
"copilot/b",
|
|
43
|
+
"opencode/c",
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("preserves the (agent, name) pair so the same name across agents stays distinct", () => {
|
|
48
|
+
const registry = createRegistry()
|
|
49
|
+
.register(makeWorkflow("ralph", "claude"))
|
|
50
|
+
.register(makeWorkflow("ralph", "copilot"));
|
|
51
|
+
|
|
52
|
+
const all = listWorkflows(registry);
|
|
53
|
+
expect(all).toHaveLength(2);
|
|
54
|
+
const keys = all.map((w) => `${w.agent}/${w.name}`).sort();
|
|
55
|
+
expect(keys).toEqual(["claude/ralph", "copilot/ralph"]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("getWorkflow", () => {
|
|
60
|
+
test("returns undefined when the (name, agent) pair is not registered", () => {
|
|
61
|
+
const registry = createRegistry();
|
|
62
|
+
expect(getWorkflow(registry, "claude", "missing")).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("returns undefined when only the name (but not the agent) matches", () => {
|
|
66
|
+
const registry = createRegistry().register(makeWorkflow("ralph", "claude"));
|
|
67
|
+
// Same name, different agent — must NOT resolve.
|
|
68
|
+
expect(getWorkflow(registry, "copilot", "ralph")).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("resolves the matching (name, agent) pair", () => {
|
|
72
|
+
const wf = makeWorkflow("ralph", "claude");
|
|
73
|
+
const registry = createRegistry().register(wf);
|
|
74
|
+
const result = getWorkflow(registry, "claude", "ralph");
|
|
75
|
+
expect(result).toBeDefined();
|
|
76
|
+
expect(result!.name).toBe("ralph");
|
|
77
|
+
expect(result!.agent).toBe("claude");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("does not return a workflow registered under a different agent", () => {
|
|
81
|
+
const registry = createRegistry()
|
|
82
|
+
.register(makeWorkflow("ralph", "claude"))
|
|
83
|
+
.register(makeWorkflow("ralph", "copilot"));
|
|
84
|
+
|
|
85
|
+
const claudeRalph = getWorkflow(registry, "claude", "ralph");
|
|
86
|
+
const copilotRalph = getWorkflow(registry, "copilot", "ralph");
|
|
87
|
+
expect(claudeRalph).toBeDefined();
|
|
88
|
+
expect(copilotRalph).toBeDefined();
|
|
89
|
+
expect(claudeRalph!.agent).toBe("claude");
|
|
90
|
+
expect(copilotRalph!.agent).toBe("copilot");
|
|
91
|
+
});
|
|
92
|
+
});
|