@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,21 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session
|
|
2
|
+
* Session and status subcommand builders — atomic-CLI internal.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 "
|
|
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
|
|
27
|
-
* parent
|
|
28
|
-
*
|
|
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/
|
|
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(
|
|
39
|
-
|
|
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
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
const {
|
|
55
|
-
const {
|
|
56
|
-
const {
|
|
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.
|
|
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]!.
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -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 "
|
|
18
|
-
import type {
|
|
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(
|
|
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");
|
|
Binary file
|