From b82a59a7987bdb71ebb13960171cc4f177e62082 Mon Sep 17 00:00:00 2001 From: Omar Shahine Date: Thu, 30 Jul 2026 07:16:57 -0700 Subject: [PATCH] feat(cli): add openclaw automations alias and reword cron display prose (#114854) * feat(cli): add openclaw automations alias and reword cron display prose Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WhJ8EiMXue6ADLmHfb7FL6 * test(doctor): update cron doctor prose assertions for automations wording * fix(plugins): include command aliases in plugin CLI collision detection Codex review finding on the automations alias: plugin CLI registration seeded existingCommands from command names only, so a plugin exposing a top-level command matching an alias-only root name (automations, terminal, chat) would crash Commander at startup instead of being skipped. Seed from names plus aliases; regression test covers the alias path. * fix(cli): rename residual cron prose in CLI and gateway RPC errors Found in combined dev-gateway E2E: automation not found / unknown automation id errors, add/edit prose, docs tip, skills-cli mention, and the gateway RPC not-found message. The CLI missing-get matcher accepts both message forms so older gateways keep resolving name lookups. * fix(gateway): keep cron.get missing wording as a wire contract for older CLI matchers ClawSweeper rank-up: shipped CLI matchers parse 'cron job not found: ' before the name-lookup fallback; the rename stays CLI-display only. Adds a regression pinning the exact wire form. * fix(cli): rename doctor and task-summary cron prose flagged in review Repair-plan advisories, session-registry task summary, and the heartbeat migration health check now say automations; recreate hints use the openclaw automations CLI form. --------- Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- ...embedded-agent-subscribe.handlers.tools.ts | 3 ++- src/cli/command-catalog.ts | 2 ++ src/cli/cron-cli.test.ts | 6 +++-- src/cli/cron-cli/list-jobs.ts | 13 ++++++----- src/cli/cron-cli/register.cron-add.ts | 10 ++++----- src/cli/cron-cli/register.cron-edit.ts | 4 ++-- src/cli/cron-cli/register.cron-scratch.ts | 2 +- src/cli/cron-cli/register.cron-simple.test.ts | 4 +++- src/cli/cron-cli/register.cron-simple.ts | 16 +++++++------- src/cli/cron-cli/register.ts | 5 +++-- src/cli/cron-cli/shared.ts | 4 ++-- src/cli/program/register.subclis-core.ts | 4 +++- src/cli/program/subcli-descriptors.ts | 9 +++++++- src/cli/skills-cli.format.ts | 2 +- src/cli/skills-cli.test.ts | 2 +- src/commands/doctor/cron/index.test.ts | 22 +++++++++---------- src/commands/doctor/cron/index.ts | 12 +++++----- src/commands/doctor/cron/legacy-repair.ts | 2 +- src/commands/doctor/cron/repair-plan.ts | 8 +++---- src/commands/doctor/cron/warnings.test.ts | 2 +- src/commands/doctor/cron/warnings.ts | 8 +++---- src/commands/tasks.ts | 2 +- src/config/schema.help.automation.ts | 8 +++---- src/config/schema.hints.ts | 2 +- src/config/schema.labels.ts | 8 +++---- .../doctor-health-contributions-final.ts | 2 +- src/gateway/explicit-connection-policy.ts | 3 ++- src/gateway/server-methods/cron.ts | 2 ++ .../server-methods/cron.validation.test.ts | 16 ++++++++++++-- src/plugins/cli.test.ts | 11 ++++++++++ src/plugins/cli.ts | 4 +++- 31 files changed, 123 insertions(+), 75 deletions(-) diff --git a/src/agents/embedded-agent-subscribe.handlers.tools.ts b/src/agents/embedded-agent-subscribe.handlers.tools.ts index c9cf8e01f7e5..52aa4dd2525f 100644 --- a/src/agents/embedded-agent-subscribe.handlers.tools.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.ts @@ -597,7 +597,8 @@ function isOpenClawCronAddShellCommand(args: unknown): boolean { return ( (isOpenClawExecutable(tokens[commandIndex]) || (packageRunner.acceptsPackageSpec && isOpenClawPackageSpec(tokens[commandIndex]))) && - normalizeOptionalLowercaseString(tokens[cliArgIndex]) === "cron" && + (normalizeOptionalLowercaseString(tokens[cliArgIndex]) === "cron" || + normalizeOptionalLowercaseString(tokens[cliArgIndex]) === "automations") && (action === "add" || action === "create") && !actionArgs.some((token) => token === "-h" || token === "--help") ); diff --git a/src/cli/command-catalog.ts b/src/cli/command-catalog.ts index 8f96dba93bed..d246989a4202 100644 --- a/src/cli/command-catalog.ts +++ b/src/cli/command-catalog.ts @@ -322,6 +322,8 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [ policy: { ownsProtocolStdout: true }, }, { commandPath: ["approvals"], policy: { networkProxy: "bypass" } }, + // automations is a commander alias for cron; argv-derived command paths keep the typed token. + { commandPath: ["automations"], policy: { networkProxy: "bypass" } }, { commandPath: ["backup"], policy: { bypassConfigGuard: true, networkProxy: "bypass" } }, { commandPath: ["chat"], policy: { networkProxy: "bypass" } }, { commandPath: ["config"], policy: { networkProxy: "bypass" } }, diff --git a/src/cli/cron-cli.test.ts b/src/cli/cron-cli.test.ts index 063b677b0c99..c592f812d01d 100644 --- a/src/cli/cron-cli.test.ts +++ b/src/cli/cron-cli.test.ts @@ -654,7 +654,9 @@ describe("cron cli", () => { "Option prompt", ]); - expectRuntimeErrorContaining("Pass the cron job message either positionally or with --message"); + expectRuntimeErrorContaining( + "Pass the automation message either positionally or with --message", + ); }); it("rejects ambiguous cron add names", async () => { @@ -670,7 +672,7 @@ describe("cron cli", () => { "tick", ]); - expectRuntimeErrorContaining("Pass the cron job name either positionally or with --name"); + expectRuntimeErrorContaining("Pass the automation name either positionally or with --name"); }); it("rejects webhook delivery mixed with chat delivery on cron add", async () => { diff --git a/src/cli/cron-cli/list-jobs.ts b/src/cli/cron-cli/list-jobs.ts index 401ef6fbc69d..ac602740e964 100644 --- a/src/cli/cron-cli/list-jobs.ts +++ b/src/cli/cron-cli/list-jobs.ts @@ -176,20 +176,20 @@ export async function listCronJobsFromGateway( page.nextOffset <= offset || (total !== undefined && page.nextOffset !== offset + page.jobs.length) ) { - throw new Error("cron.list pagination did not advance while looking up cron job"); + throw new Error("cron.list pagination did not advance while looking up automation"); } offset = page.nextOffset; } if (!snapshotChanged) { - throw new Error("cron.list pagination exceeded maximum pages while looking up cron job"); + throw new Error("cron.list pagination exceeded maximum pages while looking up automation"); } if (restart === CRON_LIST_MAX_SNAPSHOT_RESTARTS) { - throw new Error("cron.list inventory changed repeatedly while reading cron jobs"); + throw new Error("cron.list inventory changed repeatedly while reading automations"); } } - throw new Error("cron.list inventory changed repeatedly while reading cron jobs"); + throw new Error("cron.list inventory changed repeatedly while reading automations"); } function isMissingCronGetError(error: unknown, id: string): error is Error { @@ -198,7 +198,10 @@ function isMissingCronGetError(error: unknown, id: string): error is Error { (error instanceof Error && error.name === "GatewayClientRequestError" && (error as Error & { gatewayCode?: unknown }).gatewayCode === "INVALID_REQUEST" && - error.message.includes(`cron job not found: ${id}`)) + // Gateways emit the stable "cron job not found" wire wording (kept for older + // shipped CLI matchers); also accept the renamed form in case it ever changes. + (error.message.includes(`automation not found: ${id}`) || + error.message.includes(`cron job not found: ${id}`))) ); } diff --git a/src/cli/cron-cli/register.cron-add.ts b/src/cli/cron-cli/register.cron-add.ts index c943ca4a3840..84e259105300 100644 --- a/src/cli/cron-cli/register.cron-add.ts +++ b/src/cli/cron-cli/register.cron-add.ts @@ -34,7 +34,7 @@ export function registerCronStatusCommand(cron: Command) { addGatewayClientOptions( cron .command("status") - .description("Show cron scheduler status") + .description("Show automations scheduler status") .option("--json", "Output JSON", false) .action(async (opts) => { try { @@ -51,7 +51,7 @@ export function registerCronListCommand(cron: Command) { addGatewayClientOptions( cron .command("list") - .description("List cron jobs") + .description("List automations") .option("--all", "Include disabled jobs", false) .option("--agent ", "Filter by agent id") .option("--json", "Output JSON", false) @@ -84,7 +84,7 @@ export function registerCronAddCommand(cron: Command) { cron .command("add") .alias("create") - .description("Add a cron job") + .description("Add an automation") .argument("[scheduleOrName]", "Schedule string, or job name when using --at/--every/--cron") .argument("[message]", "Agent message when using a positional schedule") .option("--name ", "Job name") @@ -224,7 +224,7 @@ export function registerCronAddCommand(cron: Command) { const toolsAllow = parseCronToolsAllow(opts.tools); if (optionMessage && positionalMessage && optionMessage !== positionalMessage) { throw new Error( - "Pass the cron job message either positionally or with --message, not both.", + "Pass the automation message either positionally or with --message, not both.", ); } const message = optionMessage ?? positionalMessage ?? ""; @@ -435,7 +435,7 @@ export function registerCronAddCommand(cron: Command) { const positionalName = hasScheduleFlag ? normalizeOptionalString(nameArg) : undefined; if (optionName && positionalName && optionName !== positionalName) { throw new Error( - "Pass the cron job name either positionally or with --name, not both.", + "Pass the automation name either positionally or with --name, not both.", ); } const name = optionName ?? positionalName ?? ""; diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index 486479c7cd2e..4bdf3368560c 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -46,7 +46,7 @@ async function readCronJobForEdit(opts: GatewayRpcOpts, id: string): Promise job.id === id); if (!existing) { - throw new Error(`unknown cron job id: ${id}`, { cause: error }); + throw new Error(`unknown automation id: ${id}`, { cause: error }); } return existing; } @@ -56,7 +56,7 @@ export function registerCronEditCommand(cron: Command) { addGatewayClientOptions( cron .command("edit") - .description("Edit a cron job (patch fields)") + .description("Edit an automation (patch fields)") .argument("", "Job id") .option("--name ", "Set name") .option("--description ", "Set description") diff --git a/src/cli/cron-cli/register.cron-scratch.ts b/src/cli/cron-cli/register.cron-scratch.ts index 24bac9a3a971..1940dfffc3f6 100644 --- a/src/cli/cron-cli/register.cron-scratch.ts +++ b/src/cli/cron-cli/register.cron-scratch.ts @@ -29,7 +29,7 @@ export function registerCronScratchCommand(cron: Command) { addGatewayClientOptions( cron .command("scratch") - .description("Read or replace a cron job's private scratch") + .description("Read or replace an automation's private scratch") .argument("", "Job id") .option("--set ", "Replace scratch with exact text") .option("--file ", "Replace scratch from a file, or - for stdin") diff --git a/src/cli/cron-cli/register.cron-simple.test.ts b/src/cli/cron-cli/register.cron-simple.test.ts index 4f2d1f04f9b2..23d3b9b813d5 100644 --- a/src/cli/cron-cli/register.cron-simple.test.ts +++ b/src/cli/cron-cli/register.cron-simple.test.ts @@ -35,6 +35,8 @@ function mockCronShowPages(readPage: (params: { offset?: number }) => unknown): callGatewayFromCli.mockImplementation( async (method: string, _opts: unknown, params?: { id?: string; offset?: number }) => { if (method === "cron.get") { + // Mirrors the gateway's stable wire wording; older shipped CLI matchers + // parse exactly this form, so the server must not reword it. throw Object.assign(new Error(`cron job not found: ${params?.id ?? ""}`), { name: "GatewayClientRequestError", gatewayCode: "INVALID_REQUEST", @@ -161,7 +163,7 @@ describe("cron show pagination guard (regression for #83856)", () => { })); await expect(runCronShow("missing")).rejects.toThrow("exit 1"); expect(defaultRuntime.error).toHaveBeenCalledWith( - expect.stringContaining("cron job not found: missing"), + expect.stringContaining("automation not found: missing"), ); }); }); diff --git a/src/cli/cron-cli/register.cron-simple.ts b/src/cli/cron-cli/register.cron-simple.ts index 3f25defeb3f8..c958f4948569 100644 --- a/src/cli/cron-cli/register.cron-simple.ts +++ b/src/cli/cron-cli/register.cron-simple.ts @@ -131,7 +131,7 @@ export function registerCronSimpleCommands(cron: Command) { .command("rm") .alias("remove") .alias("delete") - .description("Remove a cron job") + .description("Remove an automation") .argument("", "Job id") .option("--json", "Output JSON", false) .action(async (id, opts) => { @@ -147,20 +147,20 @@ export function registerCronSimpleCommands(cron: Command) { registerCronToggleCommand({ cron, name: "enable", - description: "Enable a cron job", + description: "Enable an automation", enabled: true, }); registerCronToggleCommand({ cron, name: "disable", - description: "Disable a cron job", + description: "Disable an automation", enabled: false, }); addGatewayClientOptions( cron .command("get") - .description("Get a cron job as JSON") + .description("Get an automation as JSON") .argument("", "Job id") .action(async (id, opts) => { try { @@ -175,7 +175,7 @@ export function registerCronSimpleCommands(cron: Command) { addGatewayClientOptions( cron .command("show") - .description("Show a cron job") + .description("Show an automation") .argument("", "Job id or exact name") .option("--json", "Output JSON", false) .action(async (id, opts) => { @@ -184,7 +184,7 @@ export function registerCronSimpleCommands(cron: Command) { includeDeliveryPreview: !opts.json, }); if (!job) { - throw new Error(`cron job not found: ${String(id)}`); + throw new Error(`automation not found: ${String(id)}`); } if (opts.json) { printCronJson(enrichCronJsonWithStatus(job)); @@ -200,7 +200,7 @@ export function registerCronSimpleCommands(cron: Command) { addGatewayClientOptions( cron .command("runs") - .description("Show cron run history") + .description("Show automation run history") .requiredOption("--id ", "Job id") .option("--run-id ", "Filter by cron run id") .option("--limit ", "Max entries (default 50)", "50") @@ -226,7 +226,7 @@ export function registerCronSimpleCommands(cron: Command) { addGatewayClientOptions( cron .command("run") - .description("Run a cron job now (debug)") + .description("Run an automation now (debug)") .argument("", "Job id") .option("--due", "Run only when due (default behavior in older versions)", false) .option("--wait", "Wait for the queued run to finish", false) diff --git a/src/cli/cron-cli/register.ts b/src/cli/cron-cli/register.ts index 2886c320461c..153b7fdd7316 100644 --- a/src/cli/cron-cli/register.ts +++ b/src/cli/cron-cli/register.ts @@ -17,11 +17,12 @@ import { registerCronSimpleCommands } from "./register.cron-simple.js"; export function registerCronCli(program: Command) { const cron = program .command("cron") - .description("Manage cron jobs (via Gateway)") + .alias("automations") + .description("Manage automations (via Gateway)") .addHelpText( "after", () => - `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/cron", "docs.openclaw.ai/cli/cron")}\n${theme.muted("Upgrade tip:")} run \`openclaw doctor --fix\` to normalize legacy cron job storage.\n`, + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/cron", "docs.openclaw.ai/cli/cron")}\n${theme.muted("Upgrade tip:")} run \`openclaw doctor --fix\` to normalize legacy automation storage.\n`, ); registerCronStatusCommand(cron); diff --git a/src/cli/cron-cli/shared.ts b/src/cli/cron-cli/shared.ts index a5e4444064b6..70b2e87f8bae 100644 --- a/src/cli/cron-cli/shared.ts +++ b/src/cli/cron-cli/shared.ts @@ -216,7 +216,7 @@ export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) { : ""; defaultRuntime.error( [ - "warning: cron scheduler is disabled in the Gateway; jobs are saved but will not run automatically.", + "warning: the automations scheduler is disabled in the Gateway; jobs are saved but will not run automatically.", "Re-enable with `cron.enabled: true` (or remove `cron.enabled: false`) and restart the Gateway.", store ? `store: ${store}` : "", ] @@ -442,7 +442,7 @@ export function printCronList( opts?: { deliveryPreviews?: Map }, ) { if (jobs.length === 0) { - runtime.log("No cron jobs."); + runtime.log("No automations."); return; } diff --git a/src/cli/program/register.subclis-core.ts b/src/cli/program/register.subclis-core.ts index 6f9ebbf33301..f81f18beab8b 100644 --- a/src/cli/program/register.subclis-core.ts +++ b/src/cli/program/register.subclis-core.ts @@ -195,7 +195,9 @@ const entrySpecs: readonly CommandGroupDescriptorSpec[] = [ exportName: "registerTuiCli", }, { - commandNames: ["cron"], + // automations is a commander alias on the cron command; the lazy + // router only routes names listed here, so the alias must be owned too. + commandNames: ["cron", "automations"], loadModule: () => import("../cron-cli.js"), exportName: "registerCronCli", }, diff --git a/src/cli/program/subcli-descriptors.ts b/src/cli/program/subcli-descriptors.ts index f0f7a08b159b..c353165553fa 100644 --- a/src/cli/program/subcli-descriptors.ts +++ b/src/cli/program/subcli-descriptors.ts @@ -138,7 +138,14 @@ const subCliCommandCatalog = defineCommandDescriptorCatalog([ }, { name: "cron", - description: "Manage cron jobs (via Gateway)", + description: "Manage automations (via Gateway)", + hasSubcommands: true, + machineOutput: ({ argv }) => isCronMachineOutput(argv), + parentDefaultHelp: true, + }, + { + name: "automations", + description: "Manage automations (alias for cron)", hasSubcommands: true, machineOutput: ({ argv }) => isCronMachineOutput(argv), parentDefaultHelp: true, diff --git a/src/cli/skills-cli.format.ts b/src/cli/skills-cli.format.ts index e2ca402366d2..b48479e9de19 100644 --- a/src/cli/skills-cli.format.ts +++ b/src/cli/skills-cli.format.ts @@ -432,7 +432,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp } if (commandVisible.length > 0) { lines.push( - ` ${theme.muted("Available as command:")} people, scripts, or cron jobs can call the skill explicitly.`, + ` ${theme.muted("Available as command:")} people, scripts, or automations can call the skill explicitly.`, ); } if (promptHidden.length > 0) { diff --git a/src/cli/skills-cli.test.ts b/src/cli/skills-cli.test.ts index 952cc7469f36..4744477c5c45 100644 --- a/src/cli/skills-cli.test.ts +++ b/src/cli/skills-cli.test.ts @@ -339,7 +339,7 @@ describe("skills-cli", () => { expect(output).toContain("not-assigned"); expect(output).toContain("What this means"); expect(output).toContain("the agent may still exclude it"); - expect(output).toContain("people, scripts, or cron jobs can call the skill explicitly"); + expect(output).toContain("people, scripts, or automations can call the skill explicitly"); expect(output).toContain("kept out of normal chat"); expect(output).toContain("commands/cron may still use it"); }); diff --git a/src/commands/doctor/cron/index.test.ts b/src/commands/doctor/cron/index.test.ts index 284ae6c0de6e..8a461ac1a088 100644 --- a/src/commands/doctor/cron/index.test.ts +++ b/src/commands/doctor/cron/index.test.ts @@ -406,7 +406,7 @@ describe("maybeRepairLegacyCronStore", () => { }); expect(prompter.confirm).not.toHaveBeenCalled(); - expectNoteContaining("Cron model overrides detected", "Cron"); + expectNoteContaining("Automation model overrides detected", "Cron"); expectNoteContaining("2 jobs set `payload.model`", "Cron"); expectNoteContaining("Provider namespaces: anthropic=1, openai=1", "Cron"); expectNoteContaining("2 jobs use a different model than `agents.defaults.model`", "Cron"); @@ -444,7 +444,7 @@ describe("maybeRepairLegacyCronStore", () => { prompter: makePrompter(true), }); - expectNoNoteContaining("Cron model overrides detected", "Cron"); + expectNoNoteContaining("Automation model overrides detected", "Cron"); }); it("counts alias model pins as default mismatches", async () => { @@ -503,10 +503,10 @@ describe("maybeRepairLegacyCronStore", () => { prompter, }); - expectNoteContaining("1 cron job is still marked in-flight", "Cron"); + expectNoteContaining("1 automation is still marked in-flight", "Cron"); expectNoteContaining("shows it as `running`", "Cron"); expectNoteContaining("marks such runs interrupted the next time it starts", "Cron"); - expectNoteContaining("openclaw cron show ", "Cron"); + expectNoteContaining("openclaw automations show ", "Cron"); // Observer-only: no repair prompt and the running marker is left untouched. expect(prompter.confirm).not.toHaveBeenCalled(); @@ -529,7 +529,7 @@ describe("maybeRepairLegacyCronStore", () => { prompter: makePrompter(true), }); - expectNoteContaining("2 cron jobs are still marked in-flight", "Cron"); + expectNoteContaining("2 automations are still marked in-flight", "Cron"); expectNoteContaining("shows them as `running`", "Cron"); }); @@ -564,11 +564,11 @@ describe("maybeRepairLegacyCronStore", () => { prompter, }); - expectNoteContaining("1 cron job has failed 3+ runs in a row", "Cron"); + expectNoteContaining("1 automation has failed 3+ runs in a row", "Cron"); expectNoteContaining("re-fires it on error backoff", "Cron"); expectNoteContaining("resets on the next successful run", "Cron"); expectNoteContaining("interrupted by a gateway restart", "Cron"); - expectNoteContaining("openclaw cron show ", "Cron"); + expectNoteContaining("openclaw automations show ", "Cron"); // Observer-only: no repair prompt and the failure counters stay untouched. expect(prompter.confirm).not.toHaveBeenCalled(); @@ -607,7 +607,7 @@ describe("maybeRepairLegacyCronStore", () => { prompter: makePrompter(true), }); - expectNoteContaining("2 cron jobs have failed 3+ runs in a row", "Cron"); + expectNoteContaining("2 automations have failed 3+ runs in a row", "Cron"); }); it("stays silent when failure streaks are below the threshold", async () => { @@ -1457,7 +1457,7 @@ describe("maybeRepairLegacyCronStore", () => { // isolated agentTurn job, so the misleading repair note must stay absent. expectNoNoteContaining("Cron store issues detected", "Cron"); expectNoteContaining( - "3 isolated cron jobs drive shell/process tools from the agent prompt and keep running as-is: `Shell prompt job 1`, `Shell prompt job 2`, `Shell prompt job 3`.", + "3 isolated automations drive shell/process tools from the agent prompt and keep running as-is: `Shell prompt job 1`, `Shell prompt job 2`, `Shell prompt job 3`.", "Cron", ); expectNoteContaining("informational only", "Cron"); @@ -1524,11 +1524,11 @@ describe("maybeRepairLegacyCronStore", () => { expectNoNoteContaining("Cron store issues detected", "Cron"); expectNoteContaining( - "1 isolated cron job describes a shell command in the agent prompt but lacks shell/process tool access: `Restricted command prompt`.", + "1 isolated automation describes a shell command in the agent prompt but lacks shell/process tool access: `Restricted command prompt`.", "Cron", ); expectNoteContaining("not the supported shell-tool prompt shape", "Cron"); - expectNoteContaining("Recreate the job as a command cron job", "Cron"); + expectNoteContaining("Recreate it as a command automation", "Cron"); expectNoNoteContaining("informational only", "Cron"); expectNoNoteContaining("keep running as-is", "Cron"); expectNoNoteContaining("openclaw doctor --fix", "Cron"); diff --git a/src/commands/doctor/cron/index.ts b/src/commands/doctor/cron/index.ts index 41afc2fd1062..b74657524d31 100644 --- a/src/commands/doctor/cron/index.ts +++ b/src/commands/doctor/cron/index.ts @@ -214,10 +214,10 @@ export async function collectLegacyCronStoreHealthFindings(params: { if (names.length > 0) { findings.push( legacyCronStoreFinding({ - message: `${pluralize(names.length, "tool-bearing cron job")} ${description}.`, + message: `${pluralize(names.length, "tool-bearing automation")} ${description}.`, path: storePath, requirement, - fixHint: `Review with ${formatCliCommand("openclaw cron list")} and reauthorize with ${formatCliCommand("openclaw cron edit --tools ")}.`, + fixHint: `Review with ${formatCliCommand("openclaw automations list")} and reauthorize with ${formatCliCommand("openclaw automations edit --tools ")}.`, }), ); } @@ -360,9 +360,9 @@ export async function maybeRepairLegacyCronStore(params: { const subject = inFlightCount === 1 ? "it" : "them"; note( [ - `${pluralize(inFlightCount, "cron job")} ${inFlightCount === 1 ? "is" : "are"} still marked in-flight (\`state.runningAtMs\` is set), so ${formatCliCommand("openclaw cron list")} shows ${subject} as \`running\`.`, + `${pluralize(inFlightCount, "automation")} ${inFlightCount === 1 ? "is" : "are"} still marked in-flight (\`state.runningAtMs\` is set), so ${formatCliCommand("openclaw automations list")} shows ${subject} as \`running\`.`, `- If no gateway is currently executing ${subject}, the marker is left over from an interrupted run; the gateway marks such runs interrupted the next time it starts.`, - `- Review with ${formatCliCommand("openclaw cron list")} or ${formatCliCommand("openclaw cron show ")}.`, + `- Review with ${formatCliCommand("openclaw automations list")} or ${formatCliCommand("openclaw automations show ")}.`, ].join("\n"), "Cron", ); @@ -372,9 +372,9 @@ export async function maybeRepairLegacyCronStore(params: { if (chronicFailureCount > 0) { note( [ - `${pluralize(chronicFailureCount, "cron job")} ${chronicFailureCount === 1 ? "has" : "have"} failed ${CHRONIC_FAILURE_MIN_CONSECUTIVE_ERRORS}+ runs in a row (\`state.consecutiveErrors\`), so the scheduler only re-fires ${chronicFailureCount === 1 ? "it" : "them"} on error backoff.`, + `${pluralize(chronicFailureCount, "automation")} ${chronicFailureCount === 1 ? "has" : "have"} failed ${CHRONIC_FAILURE_MIN_CONSECUTIVE_ERRORS}+ runs in a row (\`state.consecutiveErrors\`), so the scheduler only re-fires ${chronicFailureCount === 1 ? "it" : "them"} on error backoff.`, `- The count resets on the next successful run and also counts runs interrupted by a gateway restart, so a lasting streak means repeated task failures, repeatedly interrupted runs, or a mix. Failure alerts are opt-in, so this may be the only notice.`, - `- Review with ${formatCliCommand("openclaw cron list")} or ${formatCliCommand("openclaw cron show ")}.`, + `- Review with ${formatCliCommand("openclaw automations list")} or ${formatCliCommand("openclaw automations show ")}.`, ].join("\n"), "Cron", ); diff --git a/src/commands/doctor/cron/legacy-repair.ts b/src/commands/doctor/cron/legacy-repair.ts index ee5ef61ac82b..688eb9ec160e 100644 --- a/src/commands/doctor/cron/legacy-repair.ts +++ b/src/commands/doctor/cron/legacy-repair.ts @@ -271,7 +271,7 @@ export async function applyLegacyCronStoreRepair(params: { // claiming a finished migration; doctor re-detects the leftover and retries. for (const failure of archiveResult.failures) { warnings.push( - `Migrated cron jobs to SQLite but could not archive the legacy cron file at ${shortenHomePath(failure.path)}: ${failure.reason}. Remove it manually or rerun ${formatCliCommand("openclaw doctor --fix")} to retry.`, + `Migrated automations to SQLite but could not archive the legacy cron file at ${shortenHomePath(failure.path)}: ${failure.reason}. Remove it manually or rerun ${formatCliCommand("openclaw doctor --fix")} to retry.`, ); } } diff --git a/src/commands/doctor/cron/repair-plan.ts b/src/commands/doctor/cron/repair-plan.ts index 2e26e15913d3..83c1155c2b53 100644 --- a/src/commands/doctor/cron/repair-plan.ts +++ b/src/commands/doctor/cron/repair-plan.ts @@ -29,9 +29,9 @@ export function formatUnresolvedCommandPromptAdvisory(names: string[]): string | const describeVerb = names.length === 1 ? "describes" : "describe"; const accessVerb = names.length === 1 ? "lacks" : "lack"; return [ - `${pluralize(names.length, "isolated cron job")} ${describeVerb} a shell command in the agent prompt but ${accessVerb} shell/process tool access${formatJobNameList(names)}.`, + `${pluralize(names.length, "isolated automation")} ${describeVerb} a shell command in the agent prompt but ${accessVerb} shell/process tool access${formatJobNameList(names)}.`, "- This is not the supported shell-tool prompt shape, so doctor cannot prove the job will execute the requested command.", - '- Recreate the job as a command cron job (`openclaw cron add ... --command ""`) or grant explicit shell/process tool access before relying on it.', + '- Recreate it as a command automation (`openclaw automations add ... --command ""`) or grant explicit shell/process tool access before relying on it.', ].join("\n"); } @@ -47,9 +47,9 @@ export function formatUnresolvedShellPromptAdvisory(names: string[]): string | n const verb = names.length === 1 ? "drives" : "drive"; const keepVerb = names.length === 1 ? "keeps" : "keep"; return [ - `${pluralize(names.length, "isolated cron job")} ${verb} shell/process tools from the agent prompt and ${keepVerb} running as-is${formatJobNameList(names)}.`, + `${pluralize(names.length, "isolated automation")} ${verb} shell/process tools from the agent prompt and ${keepVerb} running as-is${formatJobNameList(names)}.`, "- This is a supported shape, not a legacy store row, so the doctor fix path cannot convert it and the finding is informational only.", - '- For a deterministic run, recreate the job as a command cron job (`openclaw cron add ... --command ""`).', + '- For a deterministic run, recreate it as a command automation (`openclaw automations add ... --command ""`).', ].join("\n"); } diff --git a/src/commands/doctor/cron/warnings.test.ts b/src/commands/doctor/cron/warnings.test.ts index 9f920220d042..72df7a8922ef 100644 --- a/src/commands/doctor/cron/warnings.test.ts +++ b/src/commands/doctor/cron/warnings.test.ts @@ -58,7 +58,7 @@ describe("collectCronDeliveryTargetAdvisory", () => { resolveAvailableChannelIds: availableChannels("slack", "telegram"), }); expect(advisory).not.toBeNull(); - expect(advisory).toContain("Cron delivery targets unavailable channels"); + expect(advisory).toContain("Automation delivery targets unavailable channels"); expect(advisory).toContain("1 job announces"); expect(advisory).toContain("Channels: missing-channel=1"); expect(advisory).toContain("Examples: report -> missing-channel"); diff --git a/src/commands/doctor/cron/warnings.ts b/src/commands/doctor/cron/warnings.ts index 3107ddd4b8a8..fa41a8baeb7c 100644 --- a/src/commands/doctor/cron/warnings.ts +++ b/src/commands/doctor/cron/warnings.ts @@ -108,7 +108,7 @@ export function noteCronModelOverrides(params: { } const lines = [ - `Cron model overrides detected at ${shortenHomePath(params.storePath)}.`, + `Automation model overrides detected at ${shortenHomePath(params.storePath)}.`, `- ${pluralize(overrideCount, "job")} set \`payload.model\` and will not inherit \`agents.defaults.model\`${defaultModel ? ` (${defaultModel})` : ""}`, `- Provider namespaces: ${formatSortedCounts(providerCounts)}`, ]; @@ -119,7 +119,7 @@ export function noteCronModelOverrides(params: { lines.push(`- Examples: ${mismatchExamples.join(", ")}`); } lines.push( - `Review with ${formatCliCommand("openclaw cron list")} and ${formatCliCommand("openclaw cron show ")}; remove \`payload.model\` from jobs that should inherit the default.`, + `Review with ${formatCliCommand("openclaw automations list")} and ${formatCliCommand("openclaw automations show ")}; remove \`payload.model\` from jobs that should inherit the default.`, ); note(lines.join("\n"), "Cron"); @@ -206,11 +206,11 @@ function collectCronDeliveryTargetAdvisory(params: { } return [ - `Cron delivery targets unavailable channels at ${shortenHomePath(params.storePath)}.`, + `Automation delivery targets unavailable channels at ${shortenHomePath(params.storePath)}.`, `- ${pluralize(unavailableCount, "job")} ${unavailableCount === 1 ? "announces" : "announce"} to a channel whose plugin is not active; the next scheduled run will fail to deliver`, `- Channels: ${formatSortedCounts(channelCounts)}`, `- Examples: ${examples.join(", ")}`, - `Reactivate the channel plugin or update the job's \`delivery.channel\` after reviewing with ${formatCliCommand("openclaw cron list")} and ${formatCliCommand("openclaw cron show ")}.`, + `Reactivate the channel plugin or update the job's \`delivery.channel\` after reviewing with ${formatCliCommand("openclaw automations list")} and ${formatCliCommand("openclaw automations show ")}.`, ].join("\n"); } diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts index 9379f14cd99f..97fe4bf0e565 100644 --- a/src/commands/tasks.ts +++ b/src/commands/tasks.ts @@ -625,7 +625,7 @@ export async function tasksMaintenanceCommand( ); runtime.log( info( - `Session registry: ${sessionMaintenance.pruned} prune · ${sessionMaintenance.runningCronJobs} running cron jobs`, + `Session registry: ${sessionMaintenance.pruned} prune · ${sessionMaintenance.runningCronJobs} running automations`, ), ); runtime.log( diff --git a/src/config/schema.help.automation.ts b/src/config/schema.help.automation.ts index ad508dba645d..84bea29e5825 100644 --- a/src/config/schema.help.automation.ts +++ b/src/config/schema.help.automation.ts @@ -84,13 +84,13 @@ export const AUTOMATION_FIELD_HELP: Record = { "Per-agent sessions-directory disk budget (for example `500mb`). Defaults to `10gb`; when exceeded, warn mode reports pressure and enforce mode performs oldest-first cleanup (archived transcripts before live sessions). Set `false` to disable.", "session.maintenance.highWaterBytes": "Target size after disk-budget cleanup (high-water mark). Defaults to 80% of maxDiskBytes; set explicitly for tighter reclaim behavior on constrained disks.", - cron: "Global scheduler settings for stored cron jobs, run concurrency, delivery fallback, and run-session retention. Keep defaults unless you are scaling job volume or integrating external webhook receivers.", + cron: "Global scheduler settings for stored automations, run concurrency, delivery fallback, and run-session retention. Keep defaults unless you are scaling automation volume or integrating external webhook receivers.", "cron.enabled": - "Enables cron job execution for stored schedules managed by the gateway. Keep enabled for normal reminder/automation flows, and disable only to pause all cron execution without deleting jobs.", + "Enables automation execution for stored schedules managed by the gateway. Keep enabled for normal reminder/automation flows, and disable only to pause all automation execution without deleting jobs.", "cron.webhookToken": - "Bearer token attached to cron webhook POST deliveries when webhook mode is used. Prefer secret/env substitution and rotate this token regularly if shared webhook endpoints are internet-reachable.", + "Bearer token attached to automation webhook POST deliveries when webhook mode is used. Prefer secret/env substitution and rotate this token regularly if shared webhook endpoints are internet-reachable.", "cron.sessionRetention": - "Controls how long completed cron run sessions are kept before pruning (`24h`, `7d`, `1h30m`, or `false` to disable pruning; default: `24h`). Use shorter retention to reduce storage growth on high-frequency schedules.", + "Controls how long completed automation run sessions are kept before pruning (`24h`, `7d`, `1h30m`, or `false` to disable pruning; default: `24h`). Use shorter retention to reduce storage growth on high-frequency schedules.", transcripts: "Core transcript capture settings for meeting notes, recording-capable agent tools, and configured live meeting auto-start sources. Meeting plugins capture durable notes by default; set enabled to false to opt out globally.", "transcripts.enabled": diff --git a/src/config/schema.hints.ts b/src/config/schema.hints.ts index 6530f8c4c15a..68ea9dc2abab 100644 --- a/src/config/schema.hints.ts +++ b/src/config/schema.hints.ts @@ -31,7 +31,7 @@ const GROUP_HINTS = [ ["messages", "Messages", 80], ["commands", "Commands", 85], ["session", "Session", 90], - ["cron", "Cron", 100], + ["cron", "Automations", 100], ["worktrees", "Worktrees", 105], ["hooks", "Hooks", 110], ["ui", "UI", 120], diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index fe99bfb1b97d..915a03f41644 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -749,10 +749,10 @@ export const FIELD_LABELS: Record = { "session.maintenance.resetArchiveRetention": "Session Reset Archive Retention", "session.maintenance.maxDiskBytes": "Session Max Disk Budget", "session.maintenance.highWaterBytes": "Session Disk High-water Target", - cron: "Cron", - "cron.enabled": "Cron Enabled", - "cron.webhookToken": "Cron Webhook Bearer Token", - "cron.sessionRetention": "Cron Session Retention", + cron: "Automations", + "cron.enabled": "Automations Enabled", + "cron.webhookToken": "Automations Webhook Bearer Token", + "cron.sessionRetention": "Automations Session Retention", transcripts: "Transcripts", "transcripts.enabled": "Transcripts Enabled", "transcripts.autoStart": "Transcripts Auto-start Sources", diff --git a/src/flows/doctor-health-contributions-final.ts b/src/flows/doctor-health-contributions-final.ts index cd5c5200046e..0c50648353e1 100644 --- a/src/flows/doctor-health-contributions-final.ts +++ b/src/flows/doctor-health-contributions-final.ts @@ -282,7 +282,7 @@ export function resolveFinalDoctorHealthContributions(params: { id: "doctor:heartbeat-task-cron-migration", label: "Heartbeat task cron migration", healthChecks: { - description: "Heartbeat scratch task blocks must migrate into cron jobs.", + description: "Heartbeat scratch task blocks must migrate into automations.", defaultEnabled: true, async detect(ctx) { const { collectHeartbeatTaskMigrationFindings } = diff --git a/src/gateway/explicit-connection-policy.ts b/src/gateway/explicit-connection-policy.ts index d66d5d822bcd..c320145726ab 100644 --- a/src/gateway/explicit-connection-policy.ts +++ b/src/gateway/explicit-connection-policy.ts @@ -25,5 +25,6 @@ export function canSkipGatewayConfigLoad(params: { /** Returns true for command families that intentionally bypass gateway config loading. */ export function isGatewayConfigBypassCommandPath(commandPath: readonly string[]): boolean { - return commandPath[0] === "cron"; + // Command paths come from raw argv, so the automations alias keeps its typed token. + return commandPath[0] === "cron" || commandPath[0] === "automations"; } diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index ac01588ea39f..a2eaf381fb30 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -514,6 +514,8 @@ export const cronHandlers: GatewayRequestHandlers = { respond( false, undefined, + // Wire contract: shipped CLI matchers parse this exact wording for the + // name-lookup fallback (isMissingCronGetError). Rename is CLI-display only. errorShape(ErrorCodes.INVALID_REQUEST, `cron job not found: ${jobId}`), ); return; diff --git a/src/gateway/server-methods/cron.validation.test.ts b/src/gateway/server-methods/cron.validation.test.ts index c137098781dc..636d6a57c9b9 100644 --- a/src/gateway/server-methods/cron.validation.test.ts +++ b/src/gateway/server-methods/cron.validation.test.ts @@ -127,7 +127,7 @@ function createCronContext(currentJobs?: CronJob | CronJob[]) { ) => { const job = jobs.find((candidate) => candidate.id === id); if (!job) { - throw new Error(`unknown cron job id: ${id}`); + throw new Error(`unknown automation id: ${id}`); } await precondition(job, Date.now()); return await update(id, patch); @@ -698,6 +698,18 @@ describe("cron method validation", () => { }); }); + it("keeps the exact cron.get missing wording older CLI matchers parse", async () => { + const { respond } = await invokeCronGet({ jobId: "missing" }); + + // Wire contract: shipped CLIs detect a missing job via + // error.message.includes(`cron job not found: ${id}`) before falling back to + // name lookup (isMissingCronGetError). Rewording the server message strands + // older clients, so pin the legacy-matcher form here. + const error = respond.mock.calls.at(-1)?.[2]; + expect(String(error?.message)).toContain("cron job not found: missing"); + expect(String(error?.message)).not.toContain("automation not found"); + }); + it("scopes cron.list to the caller agent", async () => { const context = createCronContext(createCronJob({ agentId: "ops" })); @@ -3182,7 +3194,7 @@ describe("cron method validation", () => { it("returns INVALID_REQUEST when cron.run cannot find the job", async () => { const context = createCronContext(); - context.cron.enqueueRun.mockRejectedValueOnce(new Error("unknown cron job id: missing")); + context.cron.enqueueRun.mockRejectedValueOnce(new Error("unknown automation id: missing")); const { respond } = await invokeCron("cron.run", { id: "missing" }, { context }); expect(context.cron.enqueueRun).not.toHaveBeenCalled(); diff --git a/src/plugins/cli.test.ts b/src/plugins/cli.test.ts index f46f73424f81..9347c3e7c0aa 100644 --- a/src/plugins/cli.test.ts +++ b/src/plugins/cli.test.ts @@ -189,6 +189,17 @@ describe("registerPluginCliCommands", () => { expect(mocks.otherRegister).toHaveBeenCalledTimes(1); }); + it("skips plugin CLI registrars when an existing command alias matches", async () => { + const program = createProgram(); + // Alias-only root names (e.g. cron|automations) are owned commands too. + program.command("mem-core").alias("memory"); + + await registerPluginCliCommands(program, {} as OpenClawConfig); + + expect(mocks.memoryRegister).not.toHaveBeenCalled(); + expect(mocks.otherRegister).toHaveBeenCalledTimes(1); + }); + it("forwards an explicit env to plugin loading", async () => { const env = { OPENCLAW_HOME: "/srv/openclaw-home" } as NodeJS.ProcessEnv; diff --git a/src/plugins/cli.ts b/src/plugins/cli.ts index 1dd14da8811a..ee63cb0d19b8 100644 --- a/src/plugins/cli.ts +++ b/src/plugins/cli.ts @@ -123,7 +123,9 @@ export async function registerPluginCliCommands( await registerPluginCliCommandGroups(program, entries, { mode, primary, - existingCommands: new Set(program.commands.map((cmd) => cmd.name())), + // Include aliases: alias-only root names (cron|automations, tui|terminal) + // are owned commands too; a plugin claiming one would crash registration. + existingCommands: new Set(program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()])), logger, }); }