@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.
Files changed (120) hide show
  1. package/.agents/skills/create-spec/SKILL.md +6 -3
  2. package/.agents/skills/tdd/SKILL.md +107 -0
  3. package/.agents/skills/tdd/deep-modules.md +33 -0
  4. package/.agents/skills/tdd/interface-design.md +31 -0
  5. package/.agents/skills/tdd/mocking.md +59 -0
  6. package/.agents/skills/tdd/refactoring.md +10 -0
  7. package/.agents/skills/tdd/tests.md +61 -0
  8. package/.agents/skills/workflow-creator/SKILL.md +550 -0
  9. package/.agents/skills/workflow-creator/references/agent-sessions.md +891 -0
  10. package/.agents/skills/workflow-creator/references/agent-setup-recipe.md +266 -0
  11. package/.agents/skills/workflow-creator/references/computation-and-validation.md +201 -0
  12. package/.agents/skills/workflow-creator/references/control-flow.md +470 -0
  13. package/.agents/skills/workflow-creator/references/failure-modes.md +1014 -0
  14. package/.agents/skills/workflow-creator/references/getting-started.md +392 -0
  15. package/.agents/skills/workflow-creator/references/registry-and-validation.md +141 -0
  16. package/.agents/skills/workflow-creator/references/running-workflows.md +418 -0
  17. package/.agents/skills/workflow-creator/references/session-config.md +384 -0
  18. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +356 -0
  19. package/.agents/skills/workflow-creator/references/user-input.md +234 -0
  20. package/.agents/skills/workflow-creator/references/workflow-inputs.md +392 -0
  21. package/.claude/agents/debugger.md +2 -2
  22. package/.claude/agents/reviewer.md +1 -1
  23. package/.claude/agents/worker.md +2 -2
  24. package/.github/agents/debugger.md +1 -1
  25. package/.github/agents/worker.md +1 -1
  26. package/.mcp.json +5 -1
  27. package/.opencode/agents/debugger.md +1 -1
  28. package/.opencode/agents/worker.md +1 -1
  29. package/README.md +236 -201
  30. package/dist/sdk/define-workflow.d.ts +11 -6
  31. package/dist/sdk/define-workflow.d.ts.map +1 -1
  32. package/dist/sdk/errors.d.ts +10 -0
  33. package/dist/sdk/errors.d.ts.map +1 -1
  34. package/dist/sdk/index.d.ts +21 -9
  35. package/dist/sdk/index.d.ts.map +1 -1
  36. package/dist/sdk/primitives/inputs.d.ts +36 -0
  37. package/dist/sdk/primitives/inputs.d.ts.map +1 -0
  38. package/dist/sdk/primitives/metadata.d.ts +40 -0
  39. package/dist/sdk/primitives/metadata.d.ts.map +1 -0
  40. package/dist/sdk/primitives/run.d.ts +57 -0
  41. package/dist/sdk/primitives/run.d.ts.map +1 -0
  42. package/dist/sdk/primitives/sessions.d.ts +128 -0
  43. package/dist/sdk/primitives/sessions.d.ts.map +1 -0
  44. package/dist/sdk/runtime/executor.d.ts +24 -56
  45. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  46. package/dist/sdk/runtime/orchestrator-entry.d.ts +26 -0
  47. package/dist/sdk/runtime/orchestrator-entry.d.ts.map +1 -0
  48. package/dist/sdk/runtime/tmux.d.ts +20 -0
  49. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  50. package/dist/sdk/types.d.ts +26 -86
  51. package/dist/sdk/types.d.ts.map +1 -1
  52. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -1
  53. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -1
  54. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  55. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -1
  56. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -1
  57. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
  58. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
  59. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
  60. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
  61. package/dist/sdk/workflows/index.d.ts +20 -12
  62. package/dist/sdk/workflows/index.d.ts.map +1 -1
  63. package/dist/services/config/additional-instructions.d.ts +1 -1
  64. package/dist/services/config/additional-instructions.d.ts.map +1 -1
  65. package/package.json +4 -4
  66. package/src/cli.ts +39 -56
  67. package/src/commands/builtin-registry.ts +37 -0
  68. package/src/commands/cli/chat/index.ts +1 -3
  69. package/src/{sdk → commands/cli}/management-commands.ts +15 -55
  70. package/src/commands/cli/session.ts +1 -1
  71. package/src/commands/cli/workflow-command.test.ts +250 -16
  72. package/src/commands/cli/workflow-inputs.test.ts +1 -0
  73. package/src/commands/cli/workflow-inputs.ts +13 -3
  74. package/src/commands/cli/workflow-list.test.ts +1 -0
  75. package/src/commands/cli/workflow-list.ts +0 -0
  76. package/src/commands/cli/workflow-status.ts +1 -1
  77. package/src/commands/cli/workflow.ts +191 -11
  78. package/src/sdk/define-workflow.test.ts +47 -16
  79. package/src/sdk/define-workflow.ts +24 -6
  80. package/src/sdk/errors.test.ts +11 -0
  81. package/src/sdk/errors.ts +13 -0
  82. package/src/sdk/index.test.ts +92 -0
  83. package/src/sdk/index.ts +71 -15
  84. package/src/sdk/primitives/inputs.ts +48 -0
  85. package/src/sdk/primitives/metadata.ts +63 -0
  86. package/src/sdk/primitives/run.ts +81 -0
  87. package/src/sdk/primitives/sessions.test.ts +594 -0
  88. package/src/sdk/primitives/sessions.ts +328 -0
  89. package/src/sdk/runtime/executor.ts +36 -115
  90. package/src/sdk/runtime/orchestrator-entry.ts +110 -0
  91. package/src/sdk/runtime/tmux.ts +33 -0
  92. package/src/sdk/types.ts +26 -91
  93. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -0
  94. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -0
  95. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -0
  96. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -0
  97. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -0
  98. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -0
  99. package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -0
  100. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -0
  101. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -0
  102. package/src/sdk/workflows/index.ts +68 -51
  103. package/src/services/config/additional-instructions.ts +1 -1
  104. package/.agents/skills/test-driven-development/SKILL.md +0 -371
  105. package/.agents/skills/test-driven-development/testing-anti-patterns.md +0 -299
  106. package/dist/commands/cli/session.d.ts +0 -67
  107. package/dist/commands/cli/session.d.ts.map +0 -1
  108. package/dist/commands/cli/workflow-status.d.ts +0 -63
  109. package/dist/commands/cli/workflow-status.d.ts.map +0 -1
  110. package/dist/sdk/commander.d.ts +0 -74
  111. package/dist/sdk/commander.d.ts.map +0 -1
  112. package/dist/sdk/management-commands.d.ts +0 -42
  113. package/dist/sdk/management-commands.d.ts.map +0 -1
  114. package/dist/sdk/workflow-cli.d.ts +0 -103
  115. package/dist/sdk/workflow-cli.d.ts.map +0 -1
  116. package/dist/sdk/workflows/builtin-registry.d.ts +0 -113
  117. package/dist/sdk/workflows/builtin-registry.d.ts.map +0 -1
  118. package/src/sdk/commander.ts +0 -161
  119. package/src/sdk/workflow-cli.ts +0 -409
  120. package/src/sdk/workflows/builtin-registry.ts +0 -23
@@ -1,16 +1,196 @@
1
1
  /**
2
- * Workflow CLI command — thin delegation to the SDK WorkflowCli.
2
+ * `atomic workflow` command — built directly on the SDK's primitives.
3
3
  *
4
- * The Command returned here is mounted directly into the parent program
5
- * (src/cli.ts), which attaches the `list`, `inputs`, `status`, and
6
- * `session` subcommands on top of it.
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
- import { createWorkflowCli } from "../../sdk/workflow-cli.ts";
10
- import { toCommand } from "../../sdk/commander.ts";
11
- import { createBuiltinRegistry } from "../../sdk/workflows/builtin-registry.ts";
193
+ return cmd;
194
+ }
12
195
 
13
- export const workflowCommand = toCommand(
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
- * 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`).
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
  }
@@ -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
+ });