@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,21 +1,14 @@
1
1
  /**
2
- * Session + status management subcommands for workflow CLIs.
2
+ * Session and status subcommand builders atomic-CLI internal.
3
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.
4
+ * These were previously in the SDK (`@bastani/atomic/workflows`); after
5
+ * the SDK refactor each consumer composes their own CLI shape. The
6
+ * atomic CLI uses these helpers to keep `atomic chat session ...`,
7
+ * `atomic workflow session ...`, and `atomic session ...` in lock-step.
15
8
  */
16
9
 
17
10
  import type { Command } from "@commander-js/extra-typings";
18
- import type { SessionScope } from "../commands/cli/session.ts";
11
+ import type { SessionScope } from "./session.ts";
19
12
 
20
13
  /** Commander collect helper: accumulates repeated `-a` values into an array. */
21
14
  function collectAgent(value: string, previous: string[]): string[] {
@@ -23,14 +16,9 @@ function collectAgent(value: string, previous: string[]): string[] {
23
16
  }
24
17
 
25
18
  /**
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"`.
19
+ * Attach the `session` subcommand group (`list` / `connect` / `kill`) to
20
+ * a parent command. Returns the created `session` group so callers can
21
+ * attach extra children if needed.
34
22
  */
35
23
  export function addSessionSubcommand(
36
24
  parent: Command,
@@ -50,9 +38,7 @@ export function addSessionSubcommand(
50
38
  [] as string[],
51
39
  )
52
40
  .action(async (localOpts) => {
53
- const { sessionListCommand } = await import(
54
- "../commands/cli/session.ts"
55
- );
41
+ const { sessionListCommand } = await import("./session.ts");
56
42
  const exitCode = await sessionListCommand(localOpts.agent, scope);
57
43
  process.exit(exitCode);
58
44
  });
@@ -69,15 +55,11 @@ export function addSessionSubcommand(
69
55
  )
70
56
  .action(async (sessionId, localOpts) => {
71
57
  if (sessionId) {
72
- const { sessionConnectCommand } = await import(
73
- "../commands/cli/session.ts"
74
- );
58
+ const { sessionConnectCommand } = await import("./session.ts");
75
59
  const exitCode = await sessionConnectCommand(sessionId);
76
60
  process.exit(exitCode);
77
61
  } else {
78
- const { sessionPickerCommand } = await import(
79
- "../commands/cli/session.ts"
80
- );
62
+ const { sessionPickerCommand } = await import("./session.ts");
81
63
  const exitCode = await sessionPickerCommand(localOpts.agent, scope);
82
64
  process.exit(exitCode);
83
65
  }
@@ -95,9 +77,7 @@ export function addSessionSubcommand(
95
77
  )
96
78
  .option("-y, --yes", "Skip the confirmation prompt (required for agent callers)")
97
79
  .action(async (sessionId, localOpts) => {
98
- const { sessionKillCommand } = await import(
99
- "../commands/cli/session.ts"
100
- );
80
+ const { sessionKillCommand } = await import("./session.ts");
101
81
  const exitCode = await sessionKillCommand(
102
82
  sessionId,
103
83
  localOpts.agent,
@@ -111,12 +91,7 @@ export function addSessionSubcommand(
111
91
  return sessionCmd;
112
92
  }
113
93
 
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
- */
94
+ /** Attach a top-level `status` subcommand. Mirrors `atomic workflow status`. */
120
95
  export function addStatusSubcommand(parent: Command): void {
121
96
  parent
122
97
  .command("status")
@@ -126,9 +101,7 @@ export function addStatusSubcommand(parent: Command): void {
126
101
  .argument("[session_id]", "Workflow tmux session id (omit to list all)")
127
102
  .option("--format <format>", "Output format: json | text", "json")
128
103
  .action(async (sessionId, localOpts) => {
129
- const { workflowStatusCommand } = await import(
130
- "../commands/cli/workflow-status.ts"
131
- );
104
+ const { workflowStatusCommand } = await import("./workflow-status.ts");
132
105
  const exitCode = await workflowStatusCommand({
133
106
  id: sessionId,
134
107
  format: localOpts.format === "text" ? "text" : "json",
@@ -136,16 +109,3 @@ export function addStatusSubcommand(parent: Command): void {
136
109
  process.exit(exitCode);
137
110
  });
138
111
  }
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
- }
@@ -20,7 +20,7 @@ import {
20
20
  detachAndAttachAtomic as _detachAndAttachAtomic,
21
21
  killSession as _killSession,
22
22
  SOCKET_NAME,
23
- } from "../../sdk/workflows/index.ts";
23
+ } from "../../sdk/runtime/tmux.ts";
24
24
  import type { TmuxSession, SessionType } from "../../sdk/runtime/tmux.ts";
25
25
  import type { Subprocess } from "bun";
26
26
 
@@ -35,9 +35,12 @@ import "../../sdk/registry.ts";
35
35
  // but BEFORE the dynamic import of workflow.ts below (which uses worker.ts → mock).
36
36
 
37
37
  const executeWorkflowCalls: WorkflowRunOptions[] = [];
38
- const executeWorkflowMock = mock(async (opts: WorkflowRunOptions): Promise<void> => {
39
- executeWorkflowCalls.push(opts);
40
- });
38
+ const executeWorkflowMock = mock(
39
+ async (opts: WorkflowRunOptions): Promise<{ id: string; tmuxSessionName: string }> => {
40
+ executeWorkflowCalls.push(opts);
41
+ return { id: "fake-id", tmuxSessionName: "fake-session" };
42
+ },
43
+ );
41
44
 
42
45
  // Spread real module to preserve all exports (escBash, discoverCopilotBinary, etc.)
43
46
  // so this mock doesn't break other test files that import those exports.
@@ -48,16 +51,12 @@ await mock.module("../../sdk/runtime/executor.ts", () => ({
48
51
  runOrchestrator: async () => {},
49
52
  }));
50
53
 
51
- // Build a fresh workflowCommand using the real builtin registry directly.
52
- // This avoids stale-cache issues when workflow.ts was previously loaded by
53
- // cli.ts with a mocked (fake) builtin-registry in earlier test files.
54
- const { createWorkflowCli } = await import("../../sdk/workflow-cli.ts");
55
- const { toCommand } = await import("../../sdk/commander.ts");
56
- const { createBuiltinRegistry } = await import("../../sdk/workflows/builtin-registry.ts");
57
- const workflowCommand = toCommand(
58
- createWorkflowCli(createBuiltinRegistry()),
59
- "workflow",
60
- );
54
+ // Load the workflow command after the executor is mocked. Importing
55
+ // `./workflow.ts` triggers the registry build + Commander tree
56
+ // construction inside the mocked executor sandbox.
57
+ const { workflowCommand, buildWorkflowCommand } = await import("./workflow.ts");
58
+ const { defineWorkflow } = await import("../../sdk/define-workflow.ts");
59
+ const { createRegistry } = await import("../../sdk/registry.ts");
61
60
 
62
61
  // ─── Output capture ──────────────────────────────────────────────────────────
63
62
 
@@ -107,6 +106,7 @@ beforeEach(() => {
107
106
  executeWorkflowMock.mockClear();
108
107
  executeWorkflowMock.mockImplementation(async (opts) => {
109
108
  executeWorkflowCalls.push(opts);
109
+ return { id: "fake-id", tmuxSessionName: "fake-session" };
110
110
  });
111
111
  });
112
112
  afterEach(() => {
@@ -162,7 +162,7 @@ describe("workflowCommand named mode — success", () => {
162
162
  const call = executeWorkflowCalls[0]!;
163
163
  expect(call.agent).toBe("claude");
164
164
  expect(call.inputs?.["prompt"]).toBe("fix the auth bug");
165
- expect(call.workflowKey).toBe("claude/ralph");
165
+ expect(`${call.definition.agent}/${call.definition.name}`).toBe("claude/ralph");
166
166
  });
167
167
 
168
168
  test("dispatches ralph/copilot successfully", async () => {
@@ -200,7 +200,7 @@ describe("workflowCommand named mode — success", () => {
200
200
  ]);
201
201
 
202
202
  expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
203
- expect(executeWorkflowCalls[0]!.workflowKey).toBe("claude/deep-research-codebase");
203
+ expect(`${executeWorkflowCalls[0]!.definition.agent}/${executeWorkflowCalls[0]!.definition.name}`).toBe("claude/deep-research-codebase");
204
204
  });
205
205
 
206
206
  test("--detach flag threads detach=true to executor", async () => {
@@ -263,7 +263,10 @@ describe("workflowCommand named mode — success", () => {
263
263
  ]);
264
264
 
265
265
  expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
266
- expect(executeWorkflowCalls[0]!.workflowKey).toBe("copilot/deep-research-codebase");
266
+ const c = executeWorkflowCalls[0]!;
267
+ expect(`${c.definition.agent}/${c.definition.name}`).toBe(
268
+ "copilot/deep-research-codebase",
269
+ );
267
270
  });
268
271
  });
269
272
 
@@ -382,3 +385,234 @@ describe("workflowCommand enum input coercion", () => {
382
385
  expect(executeWorkflowCalls[0]!.inputs?.["output-type"]).toBe("prototype");
383
386
  });
384
387
  });
388
+
389
+ // ─── Help fallback when name/agent is missing ────────────────────────────────
390
+ //
391
+ // `cmd.help()` is the action's terminal branch when neither `-n` nor `-a`
392
+ // can resolve to a target (and the TTY picker isn't viable). With
393
+ // `exitOverride()` Commander throws a CommanderError instead of calling
394
+ // `process.exit`, so we can assert the dispatcher reaches the help path
395
+ // without dispatching to the executor.
396
+
397
+ describe("workflowCommand help fallback", () => {
398
+ test("no name and no agent triggers cmd.help() without dispatch", async () => {
399
+ enableExitOverride();
400
+ let threw = false;
401
+ const cap = captureOutput();
402
+ try {
403
+ await workflowCommand.parseAsync(["node", "cli"]);
404
+ } catch {
405
+ threw = true;
406
+ } finally {
407
+ cap.restore();
408
+ }
409
+ expect(threw).toBe(true);
410
+ expect(executeWorkflowMock).not.toHaveBeenCalled();
411
+ });
412
+
413
+ test("agent without name does NOT trigger picker when stdout is not a TTY", async () => {
414
+ enableExitOverride();
415
+ const origIsTTY = process.stdout.isTTY;
416
+ Object.defineProperty(process.stdout, "isTTY", {
417
+ configurable: true,
418
+ get: () => false,
419
+ });
420
+ let threw = false;
421
+ const cap = captureOutput();
422
+ try {
423
+ // -a claude with no -n: in TTY mode this would launch the picker;
424
+ // with isTTY=false it falls through to cmd.help().
425
+ await workflowCommand.parseAsync(["node", "cli", "-a", "claude"]);
426
+ } catch {
427
+ threw = true;
428
+ } finally {
429
+ cap.restore();
430
+ Object.defineProperty(process.stdout, "isTTY", {
431
+ configurable: true,
432
+ get: () => origIsTTY,
433
+ });
434
+ }
435
+ expect(threw).toBe(true);
436
+ expect(executeWorkflowMock).not.toHaveBeenCalled();
437
+ });
438
+
439
+ test("name without agent triggers cmd.help() — agent is required", async () => {
440
+ enableExitOverride();
441
+ let threw = false;
442
+ const cap = captureOutput();
443
+ try {
444
+ await workflowCommand.parseAsync(["node", "cli", "-n", "ralph"]);
445
+ } catch {
446
+ threw = true;
447
+ } finally {
448
+ cap.restore();
449
+ }
450
+ expect(threw).toBe(true);
451
+ expect(executeWorkflowMock).not.toHaveBeenCalled();
452
+ });
453
+ });
454
+
455
+ // ─── Custom-registry behaviours ──────────────────────────────────────────────
456
+ //
457
+ // `buildWorkflowCommand(registry)` lets us test branches that the
458
+ // builtin registry doesn't exercise: workflows with empty input
459
+ // schemas (free-form prompt collapse), enum inputs without a
460
+ // description (fallback `desc` line), and (name, agent) pairs that
461
+ // resolve only for one agent (resolveWorkflow's hint builder).
462
+
463
+ describe("buildWorkflowCommand with custom registries", () => {
464
+ test("empty-inputs workflow + positional prompt collapses into inputs.prompt", async () => {
465
+ const freeForm = defineWorkflow({
466
+ name: "free-form",
467
+ source: import.meta.path,
468
+ })
469
+ .for("claude")
470
+ .run(async () => {})
471
+ .compile();
472
+ const registry = createRegistry().register(freeForm);
473
+ const cmd = buildWorkflowCommand(registry);
474
+
475
+ await cmd.parseAsync([
476
+ "node", "cli",
477
+ "-n", "free-form",
478
+ "-a", "claude",
479
+ "fix",
480
+ "the",
481
+ "auth",
482
+ "bug",
483
+ ]);
484
+
485
+ expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
486
+ expect(executeWorkflowCalls[0]!.inputs?.["prompt"]).toBe("fix the auth bug");
487
+ });
488
+
489
+ test("workflow with declared inputs ignores positional prompt collapsing", async () => {
490
+ const declared = defineWorkflow({
491
+ name: "declared",
492
+ source: import.meta.path,
493
+ inputs: [{ name: "topic", type: "text", required: false }],
494
+ })
495
+ .for("claude")
496
+ .run(async () => {})
497
+ .compile();
498
+ const registry = createRegistry().register(declared);
499
+ const cmd = buildWorkflowCommand(registry);
500
+
501
+ await cmd.parseAsync([
502
+ "node", "cli",
503
+ "-n", "declared",
504
+ "-a", "claude",
505
+ "trailing", "positional",
506
+ ]);
507
+
508
+ expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
509
+ // No `prompt` should be synthesised — schema is non-empty.
510
+ expect(executeWorkflowCalls[0]!.inputs?.["prompt"]).toBeUndefined();
511
+ });
512
+
513
+ test("resolveWorkflow lists alternate agents when name exists for a different agent", async () => {
514
+ const claudeOnly = defineWorkflow({
515
+ name: "only-claude",
516
+ source: import.meta.path,
517
+ inputs: [{ name: "topic", type: "text", required: false }],
518
+ })
519
+ .for("claude")
520
+ .run(async () => {})
521
+ .compile();
522
+ const registry = createRegistry().register(claudeOnly);
523
+ const cmd = buildWorkflowCommand(registry);
524
+ cmd.exitOverride();
525
+
526
+ let caught: unknown;
527
+ const cap = captureOutput();
528
+ try {
529
+ await cmd.parseAsync([
530
+ "node", "cli",
531
+ "-n", "only-claude",
532
+ "-a", "copilot",
533
+ ]);
534
+ } catch (e) {
535
+ caught = e;
536
+ } finally {
537
+ cap.restore();
538
+ }
539
+ expect(caught).toBeDefined();
540
+ const message = caught instanceof Error ? caught.message : String(caught);
541
+ // Hint should call out the agent that DOES have this workflow.
542
+ expect(message).toContain("only-claude");
543
+ expect(message).toContain("claude");
544
+ });
545
+
546
+ test("enum input without description gets a 'one of: ...' fallback in --help", async () => {
547
+ const enumWf = defineWorkflow({
548
+ name: "enum-wf",
549
+ source: import.meta.path,
550
+ inputs: [
551
+ {
552
+ name: "format",
553
+ type: "enum",
554
+ required: false,
555
+ values: ["json", "text"],
556
+ // description omitted on purpose — exercises the enum-fallback branch.
557
+ },
558
+ ],
559
+ })
560
+ .for("claude")
561
+ .run(async () => {})
562
+ .compile();
563
+ const registry = createRegistry().register(enumWf);
564
+ const cmd = buildWorkflowCommand(registry);
565
+
566
+ // Walk the registered options and find the synthesised --format.
567
+ const formatOption = cmd.options.find((o) => o.long === "--format");
568
+ expect(formatOption).toBeDefined();
569
+ expect(formatOption!.description).toBe("one of: json, text");
570
+ });
571
+
572
+ test("text input without description falls back to the type label", async () => {
573
+ const textWf = defineWorkflow({
574
+ name: "text-wf",
575
+ source: import.meta.path,
576
+ inputs: [
577
+ { name: "topic", type: "text", required: false },
578
+ ],
579
+ })
580
+ .for("claude")
581
+ .run(async () => {})
582
+ .compile();
583
+ const registry = createRegistry().register(textWf);
584
+ const cmd = buildWorkflowCommand(registry);
585
+
586
+ const topicOption = cmd.options.find((o) => o.long === "--topic");
587
+ expect(topicOption).toBeDefined();
588
+ expect(topicOption!.description).toBe("text");
589
+ });
590
+
591
+ test("empty registry rejects unknown name + agent at dispatch time with empty-registry hint", async () => {
592
+ const registry = createRegistry();
593
+ const cmd = buildWorkflowCommand(registry);
594
+ cmd.exitOverride();
595
+
596
+ let caught: unknown;
597
+ const cap = captureOutput();
598
+ try {
599
+ // With an empty registry the option parser allows any name (since
600
+ // allNames.length === 0 short-circuits the guard), so we reach
601
+ // resolveWorkflow which throws with the "no workflow named ..."
602
+ // hint.
603
+ await cmd.parseAsync([
604
+ "node", "cli",
605
+ "-n", "anything",
606
+ "-a", "claude",
607
+ ]);
608
+ } catch (e) {
609
+ caught = e;
610
+ } finally {
611
+ cap.restore();
612
+ }
613
+ expect(caught).toBeDefined();
614
+ const message = caught instanceof Error ? caught.message : String(caught);
615
+ expect(message).toContain("anything");
616
+ expect(message).toContain("registry");
617
+ });
618
+ });
@@ -170,6 +170,7 @@ function fakeDefinition(
170
170
  description,
171
171
  inputs,
172
172
  minSDKVersion: null,
173
+ source: import.meta.path,
173
174
  run: async () => {},
174
175
  } as WorkflowDefinition;
175
176
  }
@@ -14,8 +14,13 @@
14
14
 
15
15
  import { COLORS, createPainter } from "../../theme/colors.ts";
16
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";
17
+ import { createBuiltinRegistry } from "../builtin-registry.ts";
18
+ import type {
19
+ WorkflowInput,
20
+ WorkflowDefinition,
21
+ AgentType,
22
+ } from "../../sdk/index.ts";
23
+ import { getInputSchema } from "../../sdk/index.ts";
19
24
 
20
25
  export type WorkflowInputsFormat = "json" | "text";
21
26
 
@@ -236,7 +241,12 @@ export async function workflowInputsCommand(
236
241
  }
237
242
  const def = loaded.value.definition;
238
243
 
239
- const payload = buildInputsPayload(def.name, agent, def.description, def.inputs);
244
+ const payload = buildInputsPayload(
245
+ def.name,
246
+ agent,
247
+ def.description,
248
+ getInputSchema(def),
249
+ );
240
250
 
241
251
  if (format === "json") {
242
252
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
@@ -20,6 +20,7 @@ function def(agent: AgentType, name: string, description = ""): WorkflowDefiniti
20
20
  description,
21
21
  inputs: [],
22
22
  minSDKVersion: null,
23
+ source: import.meta.path,
23
24
  run: async () => {},
24
25
  } as unknown as WorkflowDefinition;
25
26
  }
Binary file
@@ -21,7 +21,7 @@ import {
21
21
  isTmuxInstalled as _isTmuxInstalled,
22
22
  listSessions as _listSessions,
23
23
  sessionExists as _sessionExists,
24
- } from "../../sdk/workflows/index.ts";
24
+ } from "../../sdk/runtime/tmux.ts";
25
25
  import {
26
26
  readSnapshot,
27
27
  workflowRunIdFromTmuxName,