ClawFlow: add linear flow control surface (#58227)

* ClawFlow: add linear flow control surface

* Flows: clear blocked metadata on resume
This commit is contained in:
Mariano
2026-03-31 10:08:50 +02:00
committed by GitHub
parent ab4ddff7f1
commit f86e5c0a08
21 changed files with 1108 additions and 8 deletions

View File

@@ -5,6 +5,9 @@ import { registerStatusHealthSessionsCommands } from "./register.status-health-s
const mocks = vi.hoisted(() => ({
statusCommand: vi.fn(),
healthCommand: vi.fn(),
flowsListCommand: vi.fn(),
flowsShowCommand: vi.fn(),
flowsCancelCommand: vi.fn(),
sessionsCommand: vi.fn(),
sessionsCleanupCommand: vi.fn(),
tasksListCommand: vi.fn(),
@@ -23,6 +26,9 @@ const mocks = vi.hoisted(() => ({
const statusCommand = mocks.statusCommand;
const healthCommand = mocks.healthCommand;
const flowsListCommand = mocks.flowsListCommand;
const flowsShowCommand = mocks.flowsShowCommand;
const flowsCancelCommand = mocks.flowsCancelCommand;
const sessionsCommand = mocks.sessionsCommand;
const sessionsCleanupCommand = mocks.sessionsCleanupCommand;
const tasksListCommand = mocks.tasksListCommand;
@@ -42,6 +48,12 @@ vi.mock("../../commands/health.js", () => ({
healthCommand: mocks.healthCommand,
}));
vi.mock("../../commands/flows.js", () => ({
flowsListCommand: mocks.flowsListCommand,
flowsShowCommand: mocks.flowsShowCommand,
flowsCancelCommand: mocks.flowsCancelCommand,
}));
vi.mock("../../commands/sessions.js", () => ({
sessionsCommand: mocks.sessionsCommand,
}));
@@ -79,6 +91,9 @@ describe("registerStatusHealthSessionsCommands", () => {
runtime.exit.mockImplementation(() => {});
statusCommand.mockResolvedValue(undefined);
healthCommand.mockResolvedValue(undefined);
flowsListCommand.mockResolvedValue(undefined);
flowsShowCommand.mockResolvedValue(undefined);
flowsCancelCommand.mockResolvedValue(undefined);
sessionsCommand.mockResolvedValue(undefined);
sessionsCleanupCommand.mockResolvedValue(undefined);
tasksListCommand.mockResolvedValue(undefined);
@@ -317,4 +332,39 @@ describe("registerStatusHealthSessionsCommands", () => {
runtime,
);
});
it("runs flows list from the parent command", async () => {
await runCli(["flows", "--json", "--status", "blocked"]);
expect(flowsListCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
status: "blocked",
}),
runtime,
);
});
it("runs flows show subcommand with lookup forwarding", async () => {
await runCli(["flows", "show", "flow-123", "--json"]);
expect(flowsShowCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "flow-123",
json: true,
}),
runtime,
);
});
it("runs flows cancel subcommand with lookup forwarding", async () => {
await runCli(["flows", "cancel", "flow-123"]);
expect(flowsCancelCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "flow-123",
}),
runtime,
);
});
});

View File

@@ -1,4 +1,5 @@
import type { Command } from "commander";
import { flowsCancelCommand, flowsListCommand, flowsShowCommand } from "../../commands/flows.js";
import { healthCommand } from "../../commands/health.js";
import { sessionsCleanupCommand } from "../../commands/sessions-cleanup.js";
import { sessionsCommand } from "../../commands/sessions.js";
@@ -373,4 +374,84 @@ export function registerStatusHealthSessionsCommands(program: Command) {
);
});
});
const flowsCmd = program
.command("flows")
.description("Inspect ClawFlow state")
.option("--json", "Output as JSON", false)
.option(
"--status <name>",
"Filter by status (queued, running, waiting, blocked, succeeded, failed, cancelled, lost)",
)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await flowsListCommand(
{
json: Boolean(opts.json),
status: opts.status as string | undefined,
},
defaultRuntime,
);
});
});
flowsCmd.enablePositionalOptions();
flowsCmd
.command("list")
.description("List tracked ClawFlow runs")
.option("--json", "Output as JSON", false)
.option(
"--status <name>",
"Filter by status (queued, running, waiting, blocked, succeeded, failed, cancelled, lost)",
)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as
| {
json?: boolean;
status?: string;
}
| undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await flowsListCommand(
{
json: Boolean(opts.json || parentOpts?.json),
status: (opts.status as string | undefined) ?? parentOpts?.status,
},
defaultRuntime,
);
});
});
flowsCmd
.command("show")
.description("Show one ClawFlow by flow id or owner session key")
.argument("<lookup>", "Flow id or owner session key")
.option("--json", "Output as JSON", false)
.action(async (lookup, opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await flowsShowCommand(
{
lookup,
json: Boolean(opts.json || parentOpts?.json),
},
defaultRuntime,
);
});
});
flowsCmd
.command("cancel")
.description("Cancel a ClawFlow and its active child tasks")
.argument("<lookup>", "Flow id or owner session key")
.action(async (lookup) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await flowsCancelCommand(
{
lookup,
},
defaultRuntime,
);
});
});
}