refactor: move tasks into bundled plugin

This commit is contained in:
Peter Steinberger
2026-03-31 14:48:22 +01:00
parent 584db0aff2
commit c75f4695b7
39 changed files with 2492 additions and 736 deletions

View File

@@ -10,12 +10,6 @@ const mocks = vi.hoisted(() => ({
flowsCancelCommand: vi.fn(),
sessionsCommand: vi.fn(),
sessionsCleanupCommand: vi.fn(),
tasksListCommand: vi.fn(),
tasksAuditCommand: vi.fn(),
tasksMaintenanceCommand: vi.fn(),
tasksShowCommand: vi.fn(),
tasksNotifyCommand: vi.fn(),
tasksCancelCommand: vi.fn(),
setVerbose: vi.fn(),
runtime: {
log: vi.fn(),
@@ -31,12 +25,6 @@ const flowsShowCommand = mocks.flowsShowCommand;
const flowsCancelCommand = mocks.flowsCancelCommand;
const sessionsCommand = mocks.sessionsCommand;
const sessionsCleanupCommand = mocks.sessionsCleanupCommand;
const tasksListCommand = mocks.tasksListCommand;
const tasksAuditCommand = mocks.tasksAuditCommand;
const tasksMaintenanceCommand = mocks.tasksMaintenanceCommand;
const tasksShowCommand = mocks.tasksShowCommand;
const tasksNotifyCommand = mocks.tasksNotifyCommand;
const tasksCancelCommand = mocks.tasksCancelCommand;
const setVerbose = mocks.setVerbose;
const runtime = mocks.runtime;
@@ -62,15 +50,6 @@ vi.mock("../../commands/sessions-cleanup.js", () => ({
sessionsCleanupCommand: mocks.sessionsCleanupCommand,
}));
vi.mock("../../commands/tasks.js", () => ({
tasksListCommand: mocks.tasksListCommand,
tasksAuditCommand: mocks.tasksAuditCommand,
tasksMaintenanceCommand: mocks.tasksMaintenanceCommand,
tasksShowCommand: mocks.tasksShowCommand,
tasksNotifyCommand: mocks.tasksNotifyCommand,
tasksCancelCommand: mocks.tasksCancelCommand,
}));
vi.mock("../../globals.js", () => ({
setVerbose: mocks.setVerbose,
}));
@@ -96,12 +75,6 @@ describe("registerStatusHealthSessionsCommands", () => {
flowsCancelCommand.mockResolvedValue(undefined);
sessionsCommand.mockResolvedValue(undefined);
sessionsCleanupCommand.mockResolvedValue(undefined);
tasksListCommand.mockResolvedValue(undefined);
tasksAuditCommand.mockResolvedValue(undefined);
tasksMaintenanceCommand.mockResolvedValue(undefined);
tasksShowCommand.mockResolvedValue(undefined);
tasksNotifyCommand.mockResolvedValue(undefined);
tasksCancelCommand.mockResolvedValue(undefined);
});
it("runs status command with timeout and debug-derived verbose", async () => {
@@ -249,90 +222,6 @@ describe("registerStatusHealthSessionsCommands", () => {
);
});
it("runs tasks list from the parent command", async () => {
await runCli(["tasks", "--json", "--runtime", "acp", "--status", "running"]);
expect(tasksListCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
runtime: "acp",
status: "running",
}),
runtime,
);
});
it("runs tasks show subcommand with lookup forwarding", async () => {
await runCli(["tasks", "show", "run-123", "--json"]);
expect(tasksShowCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
json: true,
}),
runtime,
);
});
it("runs tasks maintenance subcommand with apply forwarding", async () => {
await runCli(["tasks", "--json", "maintenance", "--apply"]);
expect(tasksMaintenanceCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
apply: true,
}),
runtime,
);
});
it("runs tasks audit subcommand with filters", async () => {
await runCli([
"tasks",
"--json",
"audit",
"--severity",
"error",
"--code",
"stale_running",
"--limit",
"5",
]);
expect(tasksAuditCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
severity: "error",
code: "stale_running",
limit: 5,
}),
runtime,
);
});
it("runs tasks notify subcommand with lookup and policy forwarding", async () => {
await runCli(["tasks", "notify", "run-123", "state_changes"]);
expect(tasksNotifyCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
notify: "state_changes",
}),
runtime,
);
});
it("runs tasks cancel subcommand with lookup forwarding", async () => {
await runCli(["tasks", "cancel", "run-123"]);
expect(tasksCancelCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
}),
runtime,
);
});
it("runs flows list from the parent command", async () => {
await runCli(["flows", "--json", "--status", "blocked"]);

View File

@@ -4,28 +4,24 @@ import { healthCommand } from "../../commands/health.js";
import { sessionsCleanupCommand } from "../../commands/sessions-cleanup.js";
import { sessionsCommand } from "../../commands/sessions.js";
import { statusCommand } from "../../commands/status.js";
import {
tasksAuditCommand,
tasksCancelCommand,
tasksListCommand,
tasksMaintenanceCommand,
tasksNotifyCommand,
tasksShowCommand,
} from "../../commands/tasks.js";
import { setVerbose } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
import { formatHelpExamples } from "../help-format.js";
import { parsePositiveIntOrUndefined } from "./helpers.js";
function resolveVerbose(opts: { verbose?: boolean; debug?: boolean }): boolean {
return Boolean(opts.verbose || opts.debug);
}
function parseTimeoutMs(timeout: unknown): number | null | undefined {
const parsed = parsePositiveIntOrUndefined(timeout);
const parsedRaw =
typeof timeout === "string" && timeout.trim() ? Number.parseInt(timeout, 10) : undefined;
const parsed =
typeof parsedRaw === "number" && Number.isFinite(parsedRaw) && parsedRaw > 0
? parsedRaw
: undefined;
if (timeout !== undefined && parsed === undefined) {
defaultRuntime.error("--timeout must be a positive integer (milliseconds)");
defaultRuntime.exit(1);
@@ -222,159 +218,6 @@ export function registerStatusHealthSessionsCommands(program: Command) {
);
});
});
const tasksCmd = program
.command("tasks")
.description("Inspect durable background task state")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by kind (subagent, acp, cron, cli)")
.option(
"--status <name>",
"Filter by status (queued, running, succeeded, failed, timed_out, cancelled, lost)",
)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksListCommand(
{
json: Boolean(opts.json),
runtime: opts.runtime as string | undefined,
status: opts.status as string | undefined,
},
defaultRuntime,
);
});
});
tasksCmd.enablePositionalOptions();
tasksCmd
.command("list")
.description("List tracked background tasks")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by kind (subagent, acp, cron, cli)")
.option(
"--status <name>",
"Filter by status (queued, running, succeeded, failed, timed_out, cancelled, lost)",
)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as
| {
json?: boolean;
runtime?: string;
status?: string;
}
| undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksListCommand(
{
json: Boolean(opts.json || parentOpts?.json),
runtime: (opts.runtime as string | undefined) ?? parentOpts?.runtime,
status: (opts.status as string | undefined) ?? parentOpts?.status,
},
defaultRuntime,
);
});
});
tasksCmd
.command("audit")
.description("Show stale or broken background task runs")
.option("--json", "Output as JSON", false)
.option("--severity <level>", "Filter by severity (warn, error)")
.option(
"--code <name>",
"Filter by finding code (stale_queued, stale_running, lost, delivery_failed, missing_cleanup, inconsistent_timestamps)",
)
.option("--limit <n>", "Limit displayed findings")
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksAuditCommand(
{
json: Boolean(opts.json || parentOpts?.json),
severity: opts.severity as "warn" | "error" | undefined,
code: opts.code as
| "stale_queued"
| "stale_running"
| "lost"
| "delivery_failed"
| "missing_cleanup"
| "inconsistent_timestamps"
| undefined,
limit: parsePositiveIntOrUndefined(opts.limit),
},
defaultRuntime,
);
});
});
tasksCmd
.command("maintenance")
.description("Preview or apply task ledger maintenance")
.option("--json", "Output as JSON", false)
.option("--apply", "Apply reconciliation, cleanup stamping, and pruning", false)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksMaintenanceCommand(
{
json: Boolean(opts.json || parentOpts?.json),
apply: Boolean(opts.apply),
},
defaultRuntime,
);
});
});
tasksCmd
.command("show")
.description("Show one background task by task id, run id, or session key")
.argument("<lookup>", "Task id, run id, or 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 tasksShowCommand(
{
lookup,
json: Boolean(opts.json || parentOpts?.json),
},
defaultRuntime,
);
});
});
tasksCmd
.command("notify")
.description("Set task notify policy")
.argument("<lookup>", "Task id, run id, or session key")
.argument("<notify>", "Notify policy (done_only, state_changes, silent)")
.action(async (lookup, notify) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksNotifyCommand(
{
lookup,
notify: notify as "done_only" | "state_changes" | "silent",
},
defaultRuntime,
);
});
});
tasksCmd
.command("cancel")
.description("Cancel a running background task")
.argument("<lookup>", "Task id, run id, or session key")
.action(async (lookup) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksCancelCommand(
{
lookup,
},
defaultRuntime,
);
});
});
const flowsCmd = program
.command("flows")
.description("Inspect ClawFlow state")