@bastani/atomic 0.5.34 → 0.6.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +329 -50
  2. package/dist/commands/cli/session.d.ts +67 -0
  3. package/dist/commands/cli/session.d.ts.map +1 -0
  4. package/dist/commands/cli/workflow-status.d.ts +63 -0
  5. package/dist/commands/cli/workflow-status.d.ts.map +1 -0
  6. package/dist/sdk/commander.d.ts +74 -0
  7. package/dist/sdk/commander.d.ts.map +1 -0
  8. package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
  9. package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
  10. package/dist/sdk/define-workflow.d.ts +18 -9
  11. package/dist/sdk/define-workflow.d.ts.map +1 -1
  12. package/dist/sdk/index.d.ts +4 -3
  13. package/dist/sdk/index.d.ts.map +1 -1
  14. package/dist/sdk/management-commands.d.ts +42 -0
  15. package/dist/sdk/management-commands.d.ts.map +1 -0
  16. package/dist/sdk/registry.d.ts +27 -0
  17. package/dist/sdk/registry.d.ts.map +1 -0
  18. package/dist/sdk/runtime/attached-footer.d.ts +1 -1
  19. package/dist/sdk/runtime/executor-env.d.ts +20 -0
  20. package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
  21. package/dist/sdk/runtime/executor.d.ts +61 -10
  22. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  23. package/dist/sdk/types.d.ts +147 -4
  24. package/dist/sdk/types.d.ts.map +1 -1
  25. package/dist/sdk/worker-shared.d.ts +42 -0
  26. package/dist/sdk/worker-shared.d.ts.map +1 -0
  27. package/dist/sdk/workflow-cli.d.ts +103 -0
  28. package/dist/sdk/workflow-cli.d.ts.map +1 -0
  29. package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
  30. package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
  31. package/dist/sdk/workflows/index.d.ts +5 -5
  32. package/dist/sdk/workflows/index.d.ts.map +1 -1
  33. package/package.json +12 -8
  34. package/src/cli.ts +85 -144
  35. package/src/commands/cli/chat/index.ts +10 -0
  36. package/src/commands/cli/workflow-command.test.ts +279 -938
  37. package/src/commands/cli/workflow-inputs.test.ts +41 -11
  38. package/src/commands/cli/workflow-inputs.ts +47 -12
  39. package/src/commands/cli/workflow-list.test.ts +234 -0
  40. package/src/commands/cli/workflow-list.ts +0 -0
  41. package/src/commands/cli/workflow.ts +11 -798
  42. package/src/scripts/constants.ts +2 -1
  43. package/src/sdk/commander.ts +161 -0
  44. package/src/sdk/components/workflow-picker-panel.tsx +78 -258
  45. package/src/sdk/define-workflow.test.ts +104 -11
  46. package/src/sdk/define-workflow.ts +47 -11
  47. package/src/sdk/errors.test.ts +16 -0
  48. package/src/sdk/index.ts +8 -8
  49. package/src/sdk/management-commands.ts +151 -0
  50. package/src/sdk/registry.ts +132 -0
  51. package/src/sdk/runtime/attached-footer.ts +1 -1
  52. package/src/sdk/runtime/executor-env.ts +45 -0
  53. package/src/sdk/runtime/executor.test.ts +37 -0
  54. package/src/sdk/runtime/executor.ts +147 -68
  55. package/src/sdk/types.ts +169 -4
  56. package/src/sdk/worker-shared.test.ts +163 -0
  57. package/src/sdk/worker-shared.ts +155 -0
  58. package/src/sdk/workflow-cli.ts +409 -0
  59. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
  60. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
  61. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
  62. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
  63. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
  64. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
  65. package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
  66. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
  67. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
  68. package/src/sdk/workflows/builtin-registry.ts +23 -0
  69. package/src/sdk/workflows/index.ts +10 -20
  70. package/src/services/system/auth.test.ts +63 -1
  71. package/.agents/skills/workflow-creator/SKILL.md +0 -334
  72. package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
  73. package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
  74. package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
  75. package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
  76. package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
  77. package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
  78. package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
  79. package/.agents/skills/workflow-creator/references/session-config.md +0 -384
  80. package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
  81. package/.agents/skills/workflow-creator/references/user-input.md +0 -234
  82. package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
  83. package/dist/sdk/runtime/discovery.d.ts +0 -132
  84. package/dist/sdk/runtime/discovery.d.ts.map +0 -1
  85. package/dist/sdk/runtime/executor-entry.d.ts +0 -11
  86. package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
  87. package/dist/sdk/runtime/loader.d.ts +0 -70
  88. package/dist/sdk/runtime/loader.d.ts.map +0 -1
  89. package/dist/version.d.ts +0 -2
  90. package/dist/version.d.ts.map +0 -1
  91. package/src/commands/cli/workflow.test.ts +0 -317
  92. package/src/sdk/runtime/discovery.ts +0 -368
  93. package/src/sdk/runtime/executor-entry.ts +0 -18
  94. package/src/sdk/runtime/loader.ts +0 -267
@@ -12,12 +12,9 @@ import {
12
12
  renderInputsText,
13
13
  workflowInputsCommand,
14
14
  type WorkflowInputsDeps,
15
+ type ResolvedWorkflowEntry,
15
16
  } from "./workflow-inputs.ts";
16
- import type {
17
- WorkflowInput,
18
- DiscoveredWorkflow,
19
- WorkflowDefinition,
20
- } from "../../sdk/workflows/index.ts";
17
+ import type { AgentType, WorkflowInput, WorkflowDefinition } from "../../sdk/workflows/index.ts";
21
18
 
22
19
  let originalNoColor: string | undefined;
23
20
  beforeAll(() => {
@@ -153,12 +150,10 @@ function captureOutput(): {
153
150
  };
154
151
  }
155
152
 
156
- function fakeDiscovered(name: string): DiscoveredWorkflow {
153
+ function fakeDiscovered(name: string): ResolvedWorkflowEntry {
157
154
  return {
158
155
  name,
159
156
  agent: "claude",
160
- path: `/fake/path/${name}.ts`,
161
- source: "builtin",
162
157
  };
163
158
  }
164
159
 
@@ -166,10 +161,12 @@ function fakeDefinition(
166
161
  name: string,
167
162
  description: string,
168
163
  inputs: WorkflowInput[],
164
+ agent: AgentType = "claude",
169
165
  ): WorkflowDefinition {
170
166
  return {
171
167
  __brand: "WorkflowDefinition",
172
168
  name,
169
+ agent,
173
170
  description,
174
171
  inputs,
175
172
  minSDKVersion: null,
@@ -181,11 +178,9 @@ function makeDeps(overrides: Partial<WorkflowInputsDeps> = {}): WorkflowInputsDe
181
178
  return {
182
179
  findWorkflow: mock(async () => fakeDiscovered("gen-spec")) as unknown as
183
180
  WorkflowInputsDeps["findWorkflow"],
184
- loadWorkflow: mock(async (plan) => ({
181
+ loadWorkflow: mock(async () => ({
185
182
  ok: true,
186
183
  value: {
187
- ...plan,
188
- warnings: [],
189
184
  definition: fakeDefinition("gen-spec", "spec generator", [
190
185
  { name: "research_doc", type: "string", required: true },
191
186
  ]),
@@ -319,4 +314,39 @@ describe("workflowInputsCommand", () => {
319
314
  cap.restore();
320
315
  }
321
316
  });
317
+
318
+ test("default deps resolve against the builtin registry on success", async () => {
319
+ // Omit the deps argument so the module-level `defaultDeps` runs —
320
+ // this exercises `registryFindWorkflow` + `registryLoadWorkflow`.
321
+ const cap = captureOutput();
322
+ try {
323
+ const code = await workflowInputsCommand({
324
+ name: "ralph",
325
+ agent: "claude",
326
+ format: "json",
327
+ });
328
+ expect(code).toBe(0);
329
+ const parsed = JSON.parse(cap.stdout());
330
+ expect(parsed.workflow).toBe("ralph");
331
+ expect(parsed.agent).toBe("claude");
332
+ } finally {
333
+ cap.restore();
334
+ }
335
+ });
336
+
337
+ test("default deps report 'not found' for an unknown workflow", async () => {
338
+ const cap = captureOutput();
339
+ try {
340
+ const code = await workflowInputsCommand({
341
+ name: "definitely-not-a-real-workflow",
342
+ agent: "claude",
343
+ format: "json",
344
+ });
345
+ expect(code).toBe(1);
346
+ const parsed = JSON.parse(cap.stdout());
347
+ expect(parsed.error).toContain("not found");
348
+ } finally {
349
+ cap.restore();
350
+ }
351
+ });
322
352
  });
@@ -13,12 +13,9 @@
13
13
  */
14
14
 
15
15
  import { COLORS, createPainter } from "../../theme/colors.ts";
16
- import { AGENT_CONFIG, type AgentKey } from "../../services/config/index.ts";
17
- import {
18
- findWorkflow as _findWorkflow,
19
- WorkflowLoader,
20
- } from "../../sdk/workflows/index.ts";
21
- import type { WorkflowInput } from "../../sdk/workflows/index.ts";
16
+ import { AGENT_CONFIG } from "../../services/config/index.ts";
17
+ import { createBuiltinRegistry } from "../../sdk/workflows/builtin-registry.ts";
18
+ import type { WorkflowInput, WorkflowDefinition, AgentType } from "../../sdk/workflows/index.ts";
22
19
 
23
20
  export type WorkflowInputsFormat = "json" | "text";
24
21
 
@@ -151,19 +148,57 @@ export interface WorkflowInputsOptions {
151
148
  cwd?: string;
152
149
  }
153
150
 
151
+ /**
152
+ * A resolved workflow entry — enough information for the inputs command
153
+ * to load and render the workflow's declared schema.
154
+ */
155
+ export interface ResolvedWorkflowEntry {
156
+ name: string;
157
+ agent: AgentType;
158
+ }
159
+
160
+ /**
161
+ * Result of loading a workflow definition — either success with the
162
+ * definition or a failure with a stage label and message.
163
+ */
164
+ export type WorkflowLoadResult =
165
+ | { ok: true; value: { definition: WorkflowDefinition } }
166
+ | { ok: false; stage: string; error: unknown; message: string };
167
+
154
168
  /**
155
169
  * Deps for `workflowInputsCommand`. Injected so tests can drive every
156
170
  * branch (unknown agent / missing workflow / load failure / success)
157
- * without the SDK's real filesystem-dependent discovery.
171
+ * without touching the real registry or filesystem.
158
172
  */
159
173
  export interface WorkflowInputsDeps {
160
- findWorkflow: typeof _findWorkflow;
161
- loadWorkflow: typeof WorkflowLoader.loadWorkflow;
174
+ findWorkflow: (name: string, agent: AgentType, cwd?: string) => Promise<ResolvedWorkflowEntry | null>;
175
+ loadWorkflow: (entry: ResolvedWorkflowEntry) => Promise<WorkflowLoadResult>;
176
+ }
177
+
178
+ function registryFindWorkflow(name: string, agent: AgentType): Promise<ResolvedWorkflowEntry | null> {
179
+ const registry = createBuiltinRegistry();
180
+ const wf = registry.resolve(name, agent);
181
+ if (!wf) return Promise.resolve(null);
182
+ return Promise.resolve({ name: wf.name, agent: wf.agent });
183
+ }
184
+
185
+ function registryLoadWorkflow(entry: ResolvedWorkflowEntry): Promise<WorkflowLoadResult> {
186
+ const registry = createBuiltinRegistry();
187
+ const wf = registry.resolve(entry.name, entry.agent);
188
+ if (!wf) {
189
+ return Promise.resolve({
190
+ ok: false,
191
+ stage: "resolve",
192
+ error: new Error(`Workflow not found: ${entry.agent}/${entry.name}`),
193
+ message: `Workflow not found: ${entry.agent}/${entry.name}`,
194
+ });
195
+ }
196
+ return Promise.resolve({ ok: true, value: { definition: wf } });
162
197
  }
163
198
 
164
199
  const defaultDeps: WorkflowInputsDeps = {
165
- findWorkflow: _findWorkflow,
166
- loadWorkflow: WorkflowLoader.loadWorkflow,
200
+ findWorkflow: registryFindWorkflow,
201
+ loadWorkflow: registryLoadWorkflow,
167
202
  };
168
203
 
169
204
  /**
@@ -185,7 +220,7 @@ export async function workflowInputsCommand(
185
220
  `Unknown agent '${options.agent}'. Valid agents: ${validAgents.join(", ")}`,
186
221
  );
187
222
  }
188
- const agent = options.agent as AgentKey;
223
+ const agent = options.agent as AgentType;
189
224
 
190
225
  const discovered = await deps.findWorkflow(options.name, agent, options.cwd);
191
226
  if (!discovered) {
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Unit tests for `atomic workflow list` — the subcommand that replaced
3
+ * the old dispatcher `-l/--list` flag.
4
+ *
5
+ * The command itself is thin; these tests cover the behaviors that
6
+ * would silently break if the filter logic regressed: agent filtering,
7
+ * empty results, unknown agent rejection, and the sort order that
8
+ * keeps output stable across runs.
9
+ */
10
+
11
+ import { describe, test, expect, beforeAll, beforeEach, afterAll, afterEach, mock } from "bun:test";
12
+ import { workflowListCommand } from "./workflow-list.ts";
13
+ import type { AgentType, WorkflowDefinition } from "../../sdk/workflows/index.ts";
14
+
15
+ function def(agent: AgentType, name: string, description = ""): WorkflowDefinition {
16
+ return {
17
+ __brand: "WorkflowDefinition",
18
+ agent,
19
+ name,
20
+ description,
21
+ inputs: [],
22
+ minSDKVersion: null,
23
+ run: async () => {},
24
+ } as unknown as WorkflowDefinition;
25
+ }
26
+
27
+ let originalNoColor: string | undefined;
28
+ beforeAll(() => {
29
+ originalNoColor = process.env.NO_COLOR;
30
+ process.env.NO_COLOR = "1";
31
+ });
32
+ afterAll(() => {
33
+ if (originalNoColor === undefined) delete process.env.NO_COLOR;
34
+ else process.env.NO_COLOR = originalNoColor;
35
+ });
36
+
37
+ interface Captured {
38
+ stdout: string;
39
+ stderr: string;
40
+ restore: () => void;
41
+ }
42
+
43
+ function captureOutput(): Captured {
44
+ const captured: Captured = { stdout: "", stderr: "", restore: () => {} };
45
+ const origStdout = process.stdout.write.bind(process.stdout);
46
+ const origStderr = process.stderr.write.bind(process.stderr);
47
+ process.stdout.write = ((chunk: string | Uint8Array): boolean => {
48
+ captured.stdout += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
49
+ return true;
50
+ }) as typeof process.stdout.write;
51
+ process.stderr.write = ((chunk: string | Uint8Array): boolean => {
52
+ captured.stderr += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
53
+ return true;
54
+ }) as typeof process.stderr.write;
55
+ captured.restore = () => {
56
+ process.stdout.write = origStdout;
57
+ process.stderr.write = origStderr;
58
+ };
59
+ return captured;
60
+ }
61
+
62
+ const fixture = [
63
+ def("claude", "ralph", "iterative plan/orchestrate/review/debug loop"),
64
+ def("copilot", "ralph", "iterative plan/orchestrate/review/debug loop"),
65
+ def("opencode", "ralph", "iterative plan/orchestrate/review/debug loop"),
66
+ def("claude", "deep-research-codebase", "scout -> explorer fan-out"),
67
+ def("copilot", "deep-research-codebase", "scout -> explorer fan-out"),
68
+ def("opencode", "deep-research-codebase", "scout -> explorer fan-out"),
69
+ def("claude", "open-claude-design", "design system onboarding"),
70
+ def("copilot", "open-claude-design", "design system onboarding"),
71
+ def("opencode", "open-claude-design", "design system onboarding"),
72
+ ];
73
+
74
+ let savedNoColor: string | undefined;
75
+ beforeEach(() => {
76
+ savedNoColor = process.env.NO_COLOR;
77
+ process.env.NO_COLOR = "1";
78
+ });
79
+ afterEach(() => {
80
+ if (savedNoColor === undefined) delete process.env.NO_COLOR;
81
+ else process.env.NO_COLOR = savedNoColor;
82
+ });
83
+
84
+ describe("workflowListCommand", () => {
85
+ test("no agent filter: prints every registered workflow, grouped by name", async () => {
86
+ const cap = captureOutput();
87
+ let code: number;
88
+ try {
89
+ code = await workflowListCommand({}, { list: () => fixture });
90
+ } finally {
91
+ cap.restore();
92
+ }
93
+ expect(code).toBe(0);
94
+ expect(cap.stdout).toContain("ralph");
95
+ expect(cap.stdout).toContain("deep-research-codebase");
96
+ expect(cap.stdout).toContain("open-claude-design");
97
+ // Grouping deduplicates identical (name, description) pairs, so each
98
+ // workflow name appears exactly once.
99
+ expect(cap.stdout.match(/\bralph\b/g)?.length).toBe(1);
100
+ // Agents are rendered as a badge line under each group.
101
+ expect(cap.stdout).toContain("claude");
102
+ expect(cap.stdout).toContain("copilot");
103
+ expect(cap.stdout).toContain("opencode");
104
+ expect(cap.stdout).toContain(" · ");
105
+ // The old `agent=<name>` label is gone — grouping replaces it.
106
+ expect(cap.stdout).not.toContain("agent=");
107
+ });
108
+
109
+ test("agent filter narrows to the selected agent only", async () => {
110
+ const cap = captureOutput();
111
+ let code: number;
112
+ try {
113
+ code = await workflowListCommand({ agent: "claude" }, { list: () => fixture });
114
+ } finally {
115
+ cap.restore();
116
+ }
117
+ expect(code).toBe(0);
118
+ // Every workflow with a claude variant appears.
119
+ expect(cap.stdout).toContain("ralph");
120
+ expect(cap.stdout).toContain("deep-research-codebase");
121
+ expect(cap.stdout).toContain("open-claude-design");
122
+ // Non-matching agents should not appear in the listing. We check for
123
+ // the agent-badge separator, not the strings themselves — a workflow
124
+ // named "open-claude-design" legitimately contains the token
125
+ // "claude" outside the badge list.
126
+ expect(cap.stdout).not.toContain("copilot");
127
+ expect(cap.stdout).not.toContain("opencode");
128
+ // Filtered output suppresses the agent-badge line entirely, since
129
+ // the filter itself provides the context.
130
+ expect(cap.stdout).not.toContain(" · ");
131
+ });
132
+
133
+ test("unknown agent returns non-zero and writes an error message", async () => {
134
+ const cap = captureOutput();
135
+ let code: number;
136
+ try {
137
+ code = await workflowListCommand({ agent: "bogus" }, { list: () => fixture });
138
+ } finally {
139
+ cap.restore();
140
+ }
141
+ expect(code).toBe(1);
142
+ expect(cap.stderr).toContain("Unknown agent 'bogus'");
143
+ });
144
+
145
+ test("empty registry prints a helpful placeholder", async () => {
146
+ const cap = captureOutput();
147
+ let code: number;
148
+ try {
149
+ code = await workflowListCommand({}, { list: () => [] });
150
+ } finally {
151
+ cap.restore();
152
+ }
153
+ expect(code).toBe(0);
154
+ expect(cap.stdout).toContain("No workflows registered.");
155
+ });
156
+
157
+ test("agent filter with zero matches prints the placeholder, not an error", async () => {
158
+ const cap = captureOutput();
159
+ let code: number;
160
+ try {
161
+ code = await workflowListCommand(
162
+ { agent: "opencode" },
163
+ { list: () => [def("claude", "ralph")] },
164
+ );
165
+ } finally {
166
+ cap.restore();
167
+ }
168
+ expect(code).toBe(0);
169
+ expect(cap.stdout).toContain("No workflows registered.");
170
+ });
171
+
172
+ test("groups are sorted alphabetically by workflow name", async () => {
173
+ const shuffled = [
174
+ def("opencode", "ralph"),
175
+ def("claude", "deep-research-codebase"),
176
+ def("claude", "ralph"),
177
+ def("copilot", "ralph"),
178
+ ];
179
+ const cap = captureOutput();
180
+ try {
181
+ await workflowListCommand({}, { list: () => shuffled });
182
+ } finally {
183
+ cap.restore();
184
+ }
185
+ const deepIdx = cap.stdout.indexOf("deep-research-codebase");
186
+ const ralphIdx = cap.stdout.indexOf("ralph");
187
+ expect(deepIdx).toBeGreaterThan(-1);
188
+ expect(ralphIdx).toBeGreaterThan(deepIdx);
189
+ });
190
+
191
+ test("agents within a group are sorted alphabetically", async () => {
192
+ const shuffled = [
193
+ def("opencode", "ralph", "plan"),
194
+ def("claude", "ralph", "plan"),
195
+ def("copilot", "ralph", "plan"),
196
+ ];
197
+ const cap = captureOutput();
198
+ try {
199
+ await workflowListCommand({}, { list: () => shuffled });
200
+ } finally {
201
+ cap.restore();
202
+ }
203
+ const claudeIdx = cap.stdout.indexOf("claude");
204
+ const copilotIdx = cap.stdout.indexOf("copilot");
205
+ const opencodeIdx = cap.stdout.indexOf("opencode");
206
+ expect(claudeIdx).toBeGreaterThan(-1);
207
+ expect(claudeIdx).toBeLessThan(copilotIdx);
208
+ expect(copilotIdx).toBeLessThan(opencodeIdx);
209
+ });
210
+
211
+ test("variants with differing descriptions print as separate groups", async () => {
212
+ // Two variants of `ralph` with different descriptions — the grouping
213
+ // should keep them separate so no information is lost, instead of
214
+ // silently collapsing to one arbitrary description.
215
+ const entries = [
216
+ def("claude", "ralph", "plan version A"),
217
+ def("copilot", "ralph", "plan version B"),
218
+ ];
219
+ const cap = captureOutput();
220
+ try {
221
+ await workflowListCommand({}, { list: () => entries });
222
+ } finally {
223
+ cap.restore();
224
+ }
225
+ expect(cap.stdout).toContain("plan version A");
226
+ expect(cap.stdout).toContain("plan version B");
227
+ // Two separate groups means "ralph" appears as a heading twice.
228
+ expect(cap.stdout.match(/\bralph\b/g)?.length).toBe(2);
229
+ });
230
+ });
231
+
232
+ // mock import kept at module scope to match the lint pattern used by
233
+ // other test files — suppresses an unused-import warning if any.
234
+ void mock;