From 469debdb30c3fed884ec548ee9f4ca053ae52d5e Mon Sep 17 00:00:00 2001 From: Bee <68532117+abeatrix@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:53:03 -0700 Subject: [PATCH] fix(schedules): default headless routines to yolo (#12489) * fix(schedules): default headless routines to yolo Centralize the Cline default model ID in @cline/shared while preserving the @cline/llms export. Keep explicit modes stable and disable ask_question for unattended scheduled runs. * autoapprove * fix unit test * fix(schedules): harden headless routine execution --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> --- apps/cli/src/commands/schedule/common.ts | 6 +- apps/cli/src/commands/schedule/handlers.ts | 8 +- .../src/commands/schedule/import-export.ts | 10 +- apps/cli/src/wizards/schedule/index.ts | 8 +- apps/cline-hub/src/server/schedules.ts | 15 +- apps/cline-hub/src/webview/package.json | 1 + .../views/settings/routine-view.tsx | 9 +- apps/examples/desktop-app/sidecar/commands.ts | 15 +- .../components/views/chat/chat-input-bar.tsx | 5 +- .../views/settings/routine-view.tsx | 11 +- .../webview/hooks/chat-session/constants.ts | 6 +- .../src/sdk/cline-session-factory.test.ts | 4 +- bun.lock | 1 + .../core/src/cron/runner/cron-runner.test.ts | 133 +++++++++++++++++- .../core/src/cron/runner/cron-runner.ts | 20 +-- .../cron/service/schedule-command-service.ts | 6 +- .../src/cron/service/schedule-service.test.ts | 122 +++++++++++++++- .../core/src/cron/service/schedule-service.ts | 2 +- .../src/cron/specs/cron-spec-parser.test.ts | 1 + .../src/cron/store/sqlite-cron-store.test.ts | 66 +++++++++ .../core/src/cron/store/sqlite-cron-store.ts | 8 +- .../core/src/hub/daemon/runtime-handlers.ts | 7 + .../core/src/hub/server/fetch-wiring.test.ts | 73 +++++++++- .../providers/local-provider-service.test.ts | 3 +- sdk/packages/llms/src/index.browser.ts | 1 + sdk/packages/llms/src/index.ts | 1 + .../llms/src/providers/builtins.test.ts | 10 +- sdk/packages/llms/src/providers/builtins.ts | 2 +- sdk/packages/shared/src/hub.test.ts | 25 +++- sdk/packages/shared/src/hub.ts | 32 ++++- sdk/packages/shared/src/index.browser.ts | 1 + sdk/packages/shared/src/index.ts | 1 + sdk/packages/shared/src/providers/defaults.ts | 2 + 33 files changed, 543 insertions(+), 72 deletions(-) create mode 100644 sdk/packages/shared/src/providers/defaults.ts diff --git a/apps/cli/src/commands/schedule/common.ts b/apps/cli/src/commands/schedule/common.ts index 4cf1c1650a..845638806d 100644 --- a/apps/cli/src/commands/schedule/common.ts +++ b/apps/cli/src/commands/schedule/common.ts @@ -148,8 +148,10 @@ export function isJsonPath(path: string): boolean { return path.toLowerCase().endsWith(".json"); } -export function parseMode(raw: string | undefined): "act" | "plan" | undefined { - if (raw === "act" || raw === "plan") { +export function parseMode( + raw: string | undefined, +): "act" | "plan" | "yolo" | undefined { + if (raw === "act" || raw === "plan" || raw === "yolo") { return raw; } return undefined; diff --git a/apps/cli/src/commands/schedule/handlers.ts b/apps/cli/src/commands/schedule/handlers.ts index ed13e0498e..9b6867a5b9 100644 --- a/apps/cli/src/commands/schedule/handlers.ts +++ b/apps/cli/src/commands/schedule/handlers.ts @@ -1,3 +1,4 @@ +import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared"; import type { Command } from "commander"; import { ensureSchedulerHub } from "./client"; import { @@ -9,6 +10,7 @@ import { mergeScheduleMetadata, parseJsonObjectFlag, parseList, + parseMode, resolveAddress, toPositiveInt, } from "./common"; @@ -63,8 +65,8 @@ export function registerScheduleCommands( .option("--disabled", "Create in disabled state") .option("--max-parallel ", "Max parallel executions", "1") .option("--metadata-json ", "Metadata as JSON object") - .option("--mode ", "Execution mode") - .option("--model ", "Model to use", "openai/gpt-5.3-codex") + .option("--mode ", "Execution mode", "yolo") + .option("--model ", "Model to use", CLINE_DEFAULT_MODEL_ID) .option("--provider ", "Provider ID", "cline") .option("--system-prompt ", "System prompt override") .option("--tags ", "Comma-separated tags") @@ -96,7 +98,7 @@ export function registerScheduleCommands( prompt: opts.prompt, provider: opts.provider, model: opts.model, - mode: opts.mode === "plan" ? "plan" : "act", + mode: parseMode(opts.mode) ?? "yolo", workspaceRoot: opts.workspace, cwd: opts.cwd, systemPrompt: opts.systemPrompt, diff --git a/apps/cli/src/commands/schedule/import-export.ts b/apps/cli/src/commands/schedule/import-export.ts index e26750ed92..9e87b08821 100644 --- a/apps/cli/src/commands/schedule/import-export.ts +++ b/apps/cli/src/commands/schedule/import-export.ts @@ -1,5 +1,6 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, isAbsolute, resolve } from "node:path"; +import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared"; import type { Command } from "commander"; import { ensureSchedulerHub } from "./client"; import { @@ -39,7 +40,7 @@ function resolveImportedModelSelection(parsed: Record): { modelSelection?.modelId ?? parsed.modelId ?? parsed.model ?? - "openai/gpt-5.3-codex", + CLINE_DEFAULT_MODEL_ID, ).trim(); return { provider, model }; } @@ -165,7 +166,10 @@ export function registerScheduleImportCommand( prompt: String(parsed.prompt ?? "").trim(), provider, model, - mode: parsed.mode === "plan" ? "plan" : "act", + mode: + parseMode( + typeof parsed.mode === "string" ? parsed.mode : undefined, + ) ?? "yolo", workspaceRoot, cwd: String(parsed.cwd ?? "").trim() || undefined, systemPrompt: @@ -229,7 +233,7 @@ export function registerScheduleUpdateCommand( .option("--enabled", "Enable the schedule") .option("--max-parallel ", "New max parallel executions") .option("--metadata-json ", "New metadata as JSON object") - .option("--mode ", "New execution mode") + .option("--mode ", "New execution mode") .option("--model ", "New model") .option("--name ", "New name") .option("--pause", "Pause the schedule") diff --git a/apps/cli/src/wizards/schedule/index.ts b/apps/cli/src/wizards/schedule/index.ts index 7272c2c095..a4b02d727c 100644 --- a/apps/cli/src/wizards/schedule/index.ts +++ b/apps/cli/src/wizards/schedule/index.ts @@ -1,4 +1,5 @@ import * as p from "@clack/prompts"; +import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared"; import { ensureSchedulerHub, type HubScheduleClient, @@ -135,10 +136,11 @@ async function actionCreate(client: HubScheduleClient): Promise { const mode = await p.select({ message: "Agent mode", options: [ + { value: "yolo", label: "Yolo", hint: "execute without approvals" }, { value: "act", label: "Act", hint: "execute tasks" }, { value: "plan", label: "Plan", hint: "plan only" }, ], - initialValue: "act", + initialValue: "yolo", }); if (isCancel(mode)) return; @@ -214,8 +216,8 @@ async function actionCreate(client: HubScheduleClient): Promise { cronPattern, prompt: (prompt as string).trim(), provider: provider ?? "cline", - model: model ?? "openai/gpt-5.3-codex", - mode: (mode as string) === "plan" ? "plan" : "act", + model: model ?? CLINE_DEFAULT_MODEL_ID, + mode: mode as "act" | "plan" | "yolo", workspaceRoot: (workspace as string).trim(), systemPrompt, maxIterations, diff --git a/apps/cline-hub/src/server/schedules.ts b/apps/cline-hub/src/server/schedules.ts index 47368d67b4..b32ec1bec9 100644 --- a/apps/cline-hub/src/server/schedules.ts +++ b/apps/cline-hub/src/server/schedules.ts @@ -4,8 +4,10 @@ import { HubScheduleService, } from "@cline/core"; import { + CLINE_DEFAULT_MODEL_ID, ONE_TIME_SCHEDULE_CRON_PATTERN, ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY, + readHubScheduleMode, } from "@cline/shared"; import { asTrimmedString, toPositiveInt } from "./utils"; @@ -65,10 +67,6 @@ function asTrimmedStringArray(value: unknown): string[] | undefined { return values.length > 0 ? values : undefined; } -function routineScheduleMode(value: unknown): "act" | "plan" | "yolo" { - return value === "plan" || value === "yolo" ? value : "act"; -} - export async function handleRoutineScheduleCommand( command: string, args?: Record, @@ -123,9 +121,9 @@ export async function handleRoutineScheduleCommand( prompt, modelSelection: { providerId: asTrimmedString(args?.provider) ?? "cline", - modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex", + modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID, }, - mode: routineScheduleMode(args?.mode), + mode: readHubScheduleMode(args, "yolo"), workspaceRoot: routineWorkspaceRoot, cwd: asTrimmedString(args?.cwd), systemPrompt: asTrimmedString(args?.system_prompt), @@ -141,6 +139,7 @@ export async function handleRoutineScheduleCommand( const scheduleId = asTrimmedString(args?.schedule_id); if (!scheduleId) throw new Error(`${command} requires schedule_id`); if (command === "update_routine_schedule") { + const mode = readHubScheduleMode(args); const name = asTrimmedString(args?.name); const timing = routineScheduleTiming(args); const prompt = asTrimmedString(args?.prompt); @@ -157,9 +156,9 @@ export async function handleRoutineScheduleCommand( prompt, modelSelection: { providerId: asTrimmedString(args?.provider) ?? "cline", - modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex", + modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID, }, - mode: routineScheduleMode(args?.mode), + ...(mode === undefined ? {} : { mode }), workspaceRoot: routineWorkspaceRoot, cwd: asTrimmedString(args?.cwd) ?? null, systemPrompt: diff --git a/apps/cline-hub/src/webview/package.json b/apps/cline-hub/src/webview/package.json index d92e2c21c3..fb12f6615c 100644 --- a/apps/cline-hub/src/webview/package.json +++ b/apps/cline-hub/src/webview/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@base-ui/react": "^1.3.0", + "@cline/shared": "workspace:*", "@fontsource-variable/schibsted-grotesk": "^5.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", "@rive-app/react-webgl2": "^4.27.2", diff --git a/apps/cline-hub/src/webview/src/components/views/settings/routine-view.tsx b/apps/cline-hub/src/webview/src/components/views/settings/routine-view.tsx index dbd7a573ca..eac15fedd7 100644 --- a/apps/cline-hub/src/webview/src/components/views/settings/routine-view.tsx +++ b/apps/cline-hub/src/webview/src/components/views/settings/routine-view.tsx @@ -1,6 +1,7 @@ "use client"; import { + CLINE_DEFAULT_MODEL_ID, ONE_TIME_SCHEDULE_CRON_PATTERN, ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY, } from "@cline/shared"; @@ -150,7 +151,7 @@ interface ProcessContext { } const FALLBACK_PROVIDER_MODELS: Record = { - cline: ["anthropic/claude-sonnet-4.6"], + cline: [CLINE_DEFAULT_MODEL_ID], anthropic: ["claude-sonnet-4-6"], "openai-native": ["gpt-5.3-codex"], openrouter: ["anthropic/claude-sonnet-4.6"], @@ -236,7 +237,7 @@ function getScheduleProviderModel(schedule: RoutineSchedule): { model: schedule.modelSelection?.modelId?.trim() || schedule.model?.trim() || - "openai/gpt-5.3-codex", + CLINE_DEFAULT_MODEL_ID, }; } @@ -399,7 +400,7 @@ export function RoutineSchedulesContent() { scheduleDays: ["MON", "TUE", "WED", "THU", "FRI"], prompt: "Review PRs opened yesterday and summarize issues.", provider: "cline", - model: "openai/gpt-5.3-codex", + model: CLINE_DEFAULT_MODEL_ID, workspaceRoot: "", systemPrompt: "", timeoutSeconds: "", @@ -797,7 +798,7 @@ export function RoutineSchedulesContent() { const model = asTrimmedFormString(createForm.model) || (visibleProviderModels[provider] ?? [])[0] || - "openai/gpt-6-sol"; + CLINE_DEFAULT_MODEL_ID; const systemPrompt = asTrimmedFormString(createForm.systemPrompt); const timeoutSeconds = parseOptionalPositiveInt( createForm.timeoutSeconds, diff --git a/apps/examples/desktop-app/sidecar/commands.ts b/apps/examples/desktop-app/sidecar/commands.ts index 858e527a9c..e560dbc1fd 100644 --- a/apps/examples/desktop-app/sidecar/commands.ts +++ b/apps/examples/desktop-app/sidecar/commands.ts @@ -52,9 +52,11 @@ import { updateMcpSettingsFileSync, } from "@cline/core"; import { + CLINE_DEFAULT_MODEL_ID, getClineEnvironmentConfig, ONE_TIME_SCHEDULE_CRON_PATTERN, ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY, + readHubScheduleMode, } from "@cline/shared"; import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node"; import packageJson from "../package.json"; @@ -550,10 +552,6 @@ function asTrimmedStringArray(value: unknown): string[] | undefined { return values.length > 0 ? values : undefined; } -function routineScheduleMode(value: unknown): "act" | "plan" | "yolo" { - return value === "plan" || value === "yolo" ? value : "act"; -} - async function handleRoutineScheduleCommand( command: string, args?: Record, @@ -660,9 +658,9 @@ async function handleRoutineScheduleCommand( prompt, modelSelection: { providerId: asTrimmedString(args?.provider) ?? "cline", - modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex", + modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID, }, - mode: routineScheduleMode(args?.mode), + mode: readHubScheduleMode(args, "yolo"), workspaceRoot, cwd: asTrimmedString(args?.cwd), systemPrompt: asTrimmedString(args?.system_prompt), @@ -677,6 +675,7 @@ async function handleRoutineScheduleCommand( const scheduleId = asTrimmedString(args?.schedule_id); if (!scheduleId) throw new Error(`${command} requires schedule_id`); if (command === "update_routine_schedule") { + const mode = readHubScheduleMode(args); const name = asTrimmedString(args?.name); const timing = routineScheduleTiming(args); const prompt = asTrimmedString(args?.prompt); @@ -693,9 +692,9 @@ async function handleRoutineScheduleCommand( prompt, modelSelection: { providerId: asTrimmedString(args?.provider) ?? "cline", - modelId: asTrimmedString(args?.model) ?? "openai/gpt-5.3-codex", + modelId: asTrimmedString(args?.model) ?? CLINE_DEFAULT_MODEL_ID, }, - mode: routineScheduleMode(args?.mode), + ...(mode === undefined ? {} : { mode }), workspaceRoot, cwd: asTrimmedString(args?.cwd) ?? null, systemPrompt: diff --git a/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx b/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx index 6324d8f8ec..39aefdb691 100644 --- a/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx +++ b/apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx @@ -1,5 +1,6 @@ "use client"; +import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared/browser"; import { ArrowUp, Brain, @@ -62,7 +63,7 @@ const BUILTIN_SLASH_COMMANDS: SlashCommand[] = [ ]; const FALLBACK_PROVIDER_MODELS: Record = { - cline: ["anthropic/claude-sonnet-4.6"], + cline: [CLINE_DEFAULT_MODEL_ID], anthropic: ["claude-sonnet-4-6"], "openai-native": ["gpt-5.5"], openrouter: ["anthropic/claude-sonnet-4.6"], @@ -70,7 +71,7 @@ const FALLBACK_PROVIDER_MODELS: Record = { }; const FALLBACK_PROVIDER_REASONING_MODELS: Record = { - cline: ["anthropic/claude-sonnet-4.6"], + cline: [CLINE_DEFAULT_MODEL_ID], anthropic: ["claude-sonnet-4-6"], "openai-native": ["gpt-5.5"], openrouter: ["anthropic/claude-sonnet-4.6"], diff --git a/apps/examples/desktop-app/webview/components/views/settings/routine-view.tsx b/apps/examples/desktop-app/webview/components/views/settings/routine-view.tsx index c9a0533ab0..8346fca219 100644 --- a/apps/examples/desktop-app/webview/components/views/settings/routine-view.tsx +++ b/apps/examples/desktop-app/webview/components/views/settings/routine-view.tsx @@ -1,6 +1,7 @@ "use client"; import { + CLINE_DEFAULT_MODEL_ID, ONE_TIME_SCHEDULE_CRON_PATTERN, ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY, } from "@cline/shared/browser"; @@ -166,7 +167,7 @@ interface ProcessContext { } const FALLBACK_PROVIDER_MODELS: Record = { - cline: ["anthropic/claude-sonnet-4.6"], + cline: [CLINE_DEFAULT_MODEL_ID], anthropic: ["claude-sonnet-4-6"], "openai-native": ["gpt-5.3-codex"], openrouter: ["anthropic/claude-sonnet-4.6"], @@ -245,7 +246,7 @@ function getScheduleProviderModel(schedule: RoutineSchedule): { model: schedule.modelSelection?.modelId?.trim() || schedule.model?.trim() || - "openai/gpt-5.3-codex", + CLINE_DEFAULT_MODEL_ID, }; } @@ -576,7 +577,7 @@ export function RoutineSchedulesContent({ scheduleDays: ["MON", "TUE", "WED", "THU", "FRI"], prompt: "Review PRs opened yesterday and summarize issues.", provider: "cline", - model: "openai/gpt-5.3-codex", + model: CLINE_DEFAULT_MODEL_ID, workspaceRoot: "", systemPrompt: "", timeoutSeconds: "", @@ -992,7 +993,7 @@ export function RoutineSchedulesContent({ const model = asTrimmedFormString(createForm.model) || (visibleProviderModels[provider] ?? [])[0] || - "openai/gpt-5.3-codex"; + CLINE_DEFAULT_MODEL_ID; const systemPrompt = asTrimmedFormString(createForm.systemPrompt); const timeoutSeconds = parseOptionalPositiveInt( createForm.timeoutSeconds, @@ -1013,7 +1014,7 @@ export function RoutineSchedulesContent({ prompt, provider, model, - mode: editingSchedule?.mode ?? "act", + mode: editingSchedule?.mode ?? "yolo", workspace_root: workspaceRoot, cwd: editingSchedule ? (editingSchedule.cwd ?? null) : workspaceRoot, system_prompt: editingSchedule diff --git a/apps/examples/desktop-app/webview/hooks/chat-session/constants.ts b/apps/examples/desktop-app/webview/hooks/chat-session/constants.ts index 9f67c87484..1f4b9aab8d 100644 --- a/apps/examples/desktop-app/webview/hooks/chat-session/constants.ts +++ b/apps/examples/desktop-app/webview/hooks/chat-session/constants.ts @@ -1,3 +1,4 @@ +import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared/browser"; import type { ChatSessionConfig } from "@/lib/chat-schema"; import { readModelSelectionStorageFromWindow } from "@/lib/model-selection"; import { normalizeProviderId } from "@/lib/provider-id"; @@ -16,15 +17,12 @@ export const OAUTH_MANAGED_PROVIDERS = new Set([ "openai-codex", ]); -// Default Cline model — keep in sync with @cline/llms CLINE_DEFAULT_MODEL -const CLINE_DEFAULT_MODEL = "anthropic/claude-sonnet-4.6"; - export const DEFAULT_CHAT_CONFIG: ChatSessionConfig = { sessionId: undefined, workspaceRoot: "", cwd: "", provider: "cline", - model: CLINE_DEFAULT_MODEL, + model: CLINE_DEFAULT_MODEL_ID, apiKey: process.env.CLINE_API_KEY || "", mode: "act", systemPrompt: undefined, diff --git a/apps/vscode/src/sdk/cline-session-factory.test.ts b/apps/vscode/src/sdk/cline-session-factory.test.ts index edd952d532..f5d3fcffd0 100644 --- a/apps/vscode/src/sdk/cline-session-factory.test.ts +++ b/apps/vscode/src/sdk/cline-session-factory.test.ts @@ -138,7 +138,9 @@ function makeBaseConfig(overrides: Partial = {}): CoreSession describe("getDefaultModelIdForProvider", () => { it("uses the SDK provider catalog for the Cline default model", () => { - expect(getDefaultModelIdForProvider("cline")).toBe("anthropic/claude-sonnet-4.6") + expect(getDefaultModelIdForProvider("cline")).toBe( + LlmsModels.MODEL_COLLECTIONS_BY_PROVIDER_ID.cline.provider.defaultModelId, + ) }) it("uses the generated Gemini provider default", () => { diff --git a/bun.lock b/bun.lock index f4a8874aa2..0dae260474 100644 --- a/bun.lock +++ b/bun.lock @@ -75,6 +75,7 @@ "version": "0.0.0", "dependencies": { "@base-ui/react": "^1.3.0", + "@cline/shared": "workspace:*", "@fontsource-variable/schibsted-grotesk": "^5.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", "@rive-app/react-webgl2": "^4.27.2", diff --git a/sdk/packages/core/src/cron/runner/cron-runner.test.ts b/sdk/packages/core/src/cron/runner/cron-runner.test.ts index 1802da74d5..ceea236a30 100644 --- a/sdk/packages/core/src/cron/runner/cron-runner.test.ts +++ b/sdk/packages/core/src/cron/runner/cron-runner.test.ts @@ -7,7 +7,9 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import type { ChatStartSessionRequest, CronOneOffSpec } from "@cline/shared"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { DefaultToolNames } from "../../extensions/tools/constants"; import type { HubScheduleRuntimeHandlers } from "../service/schedule-service"; import { SqliteCronStore } from "../store/sqlite-cron-store"; import { CronMaterializer } from "./cron-materializer"; @@ -15,12 +17,25 @@ import { CronRunner } from "./cron-runner"; function fakeHandlers(): { handlers: HubScheduleRuntimeHandlers; - calls: { start: number; send: number; stop: number; prompts: string[] }; + calls: { + start: number; + send: number; + stop: number; + prompts: string[]; + startRequests: ChatStartSessionRequest[]; + }; } { - const calls = { start: 0, send: 0, stop: 0, prompts: [] as string[] }; + const calls = { + start: 0, + send: 0, + stop: 0, + prompts: [] as string[], + startRequests: [] as ChatStartSessionRequest[], + }; const handlers: HubScheduleRuntimeHandlers = { - async startSession(_req) { + async startSession(req) { calls.start += 1; + calls.startRequests.push(req); return { sessionId: `sess_${calls.start}` }; }, async sendSession(_sessionId, req) { @@ -109,6 +124,16 @@ describe("CronRunner", () => { expect(calls.start).toBe(1); expect(calls.send).toBe(1); expect(calls.stop).toBe(1); + expect(calls.startRequests[0]?.mode).toBe("yolo"); + expect(calls.startRequests[0]?.toolPolicies?.["*"]).toEqual({ + autoApprove: true, + }); + expect( + calls.startRequests[0]?.toolPolicies?.[DefaultToolNames.ASK], + ).toEqual({ enabled: false, autoApprove: true }); + expect( + calls.startRequests[0]?.toolPolicies?.[DefaultToolNames.SUBMIT_AND_EXIT], + ).toEqual({ enabled: true, autoApprove: true }); const run = requireValue( store.listRuns({ specId: upserted.record.specId })[0], @@ -119,6 +144,108 @@ describe("CronRunner", () => { expect(existsSync(reportPath)).toBe(true); }); + it("falls back to yolo for an unknown mode and disables questions", async () => { + const { handlers, calls } = fakeHandlers(); + const upserted = store.upsertSpec({ + externalId: "headless-unknown", + sourcePath: "headless-unknown.md", + triggerKind: "one_off", + sourceHash: "h", + parseStatus: "valid", + spec: { + triggerKind: "one_off", + id: "headless-unknown", + title: "Headless unknown", + prompt: "Do it", + workspaceRoot, + enabled: true, + mode: "unknown" as CronOneOffSpec["mode"], + tools: [DefaultToolNames.ASK, DefaultToolNames.READ_FILES], + }, + }); + store.updateSpecNextRunAt( + upserted.record.specId, + new Date(Date.now() - 1_000).toISOString(), + ); + const runner = new CronRunner({ + store, + materializer, + runtimeHandlers: handlers, + workspaceRoot, + specs: { cronSpecsDir: cronDir }, + }); + + await runner.tick(); + await runner.dispose(); + + const request = requireValue(calls.startRequests[0]); + expect(request.mode).toBe("yolo"); + expect(request.toolPolicies?.["*"]).toEqual({ + enabled: false, + autoApprove: true, + }); + expect(request.toolPolicies?.[DefaultToolNames.READ_FILES]).toEqual({ + enabled: true, + autoApprove: true, + }); + expect(request.toolPolicies?.[DefaultToolNames.ASK]).toEqual({ + enabled: false, + autoApprove: true, + }); + expect(request.toolPolicies?.[DefaultToolNames.SUBMIT_AND_EXIT]).toEqual({ + enabled: true, + autoApprove: true, + }); + }); + + it.each([ + "act", + "plan", + "yolo", + ] as const)("preserves an explicit %s mode for scheduled runs", async (mode) => { + const { handlers, calls } = fakeHandlers(); + const upserted = store.upsertSpec({ + externalId: `explicit-${mode}`, + sourcePath: `explicit-${mode}.md`, + triggerKind: "one_off", + sourceHash: `h-${mode}`, + parseStatus: "valid", + spec: { + triggerKind: "one_off", + id: `explicit-${mode}`, + title: `Explicit ${mode}`, + prompt: "Do it", + workspaceRoot, + enabled: true, + mode, + }, + }); + store.updateSpecNextRunAt( + upserted.record.specId, + new Date(Date.now() - 1_000).toISOString(), + ); + const runner = new CronRunner({ + store, + materializer, + runtimeHandlers: handlers, + workspaceRoot, + specs: { cronSpecsDir: cronDir }, + }); + + await runner.tick(); + await runner.dispose(); + + const request = requireValue(calls.startRequests[0]); + expect(request.mode).toBe(mode); + expect(request.toolPolicies?.[DefaultToolNames.ASK]).toEqual({ + enabled: false, + autoApprove: true, + }); + expect(request.toolPolicies?.[DefaultToolNames.SUBMIT_AND_EXIT]).toEqual( + mode === "yolo" ? { enabled: true, autoApprove: true } : undefined, + ); + }); + it("marks runs failed when the runtime throws", async () => { const handlers: HubScheduleRuntimeHandlers = { async startSession() { diff --git a/sdk/packages/core/src/cron/runner/cron-runner.ts b/sdk/packages/core/src/cron/runner/cron-runner.ts index dae64867c3..dba9a53903 100644 --- a/sdk/packages/core/src/cron/runner/cron-runner.ts +++ b/sdk/packages/core/src/cron/runner/cron-runner.ts @@ -61,16 +61,20 @@ function cronExtensionEnabled( function buildToolPolicies( spec: CronSpecRecord, mode: "act" | "plan" | "yolo", -): ChatStartSessionRequest["toolPolicies"] | undefined { - if (spec.tools === undefined) { - return { "*": { autoApprove: true } }; - } - const policies: NonNullable = { - "*": { enabled: false, autoApprove: true }, - }; - for (const tool of spec.tools) { +): NonNullable { + const policies: NonNullable = + spec.tools === undefined + ? { "*": { autoApprove: true } } + : { "*": { enabled: false, autoApprove: true } }; + for (const tool of spec.tools ?? []) { policies[tool] = { enabled: true, autoApprove: true }; } + // Scheduled runs are headless, so they cannot wait for a human response. + policies[DefaultToolNames.ASK] = { + ...policies[DefaultToolNames.ASK], + enabled: false, + autoApprove: true, + }; if (mode === "yolo") { policies[DefaultToolNames.SUBMIT_AND_EXIT] = { enabled: true, diff --git a/sdk/packages/core/src/cron/service/schedule-command-service.ts b/sdk/packages/core/src/cron/service/schedule-command-service.ts index 2e023506e5..161c48c823 100644 --- a/sdk/packages/core/src/cron/service/schedule-command-service.ts +++ b/sdk/packages/core/src/cron/service/schedule-command-service.ts @@ -4,7 +4,7 @@ import type { HubScheduleCreateInput, HubScheduleUpdateInput, } from "@cline/shared"; -import { createSessionId } from "@cline/shared"; +import { createSessionId, readHubScheduleMode } from "@cline/shared"; import type { HubScheduleService } from "./schedule-service"; function okReply( @@ -158,6 +158,7 @@ export class HubScheduleCommandService { private toCreateInput( payload: Record, ): HubScheduleCreateInput { + const mode = readHubScheduleMode(payload, "yolo"); const modelSelection = payload.modelSelection && typeof payload.modelSelection === "object" && @@ -172,12 +173,14 @@ export class HubScheduleCommandService { return { ...(payload as unknown as HubScheduleCreateInput), modelSelection, + mode, }; } private toUpdateInput( payload: Record, ): HubScheduleUpdateInput { + const mode = readHubScheduleMode(payload); const modelSelection = payload.modelSelection && typeof payload.modelSelection === "object" && @@ -193,6 +196,7 @@ export class HubScheduleCommandService { return { ...(payload as unknown as HubScheduleUpdateInput), modelSelection, + ...(mode === undefined ? {} : { mode }), }; } } diff --git a/sdk/packages/core/src/cron/service/schedule-service.test.ts b/sdk/packages/core/src/cron/service/schedule-service.test.ts index b0d48d5446..0abde47300 100644 --- a/sdk/packages/core/src/cron/service/schedule-service.test.ts +++ b/sdk/packages/core/src/cron/service/schedule-service.test.ts @@ -3,10 +3,12 @@ import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { + type CronScheduleSpec, ONE_TIME_SCHEDULE_CRON_PATTERN, ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY, } from "@cline/shared"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { SqliteCronStore } from "../store/sqlite-cron-store"; import { HubScheduleCommandService } from "./schedule-command-service"; import { HubScheduleService } from "./schedule-service"; @@ -80,6 +82,7 @@ describe("HubScheduleService", () => { timeoutSeconds: 30, metadata: { delivery: { threadId: "thread-1" } }, }); + expect(created.mode).toBe("yolo"); const execution = await service.triggerScheduleNow(created.scheduleId); expect(execution?.status).toBe("success"); @@ -110,6 +113,57 @@ describe("HubScheduleService", () => { } }); + sqliteIt( + "maps missing and unknown stored modes to yolo while preserving act", + async () => { + const dbPath = await createTempDbPath(); + cleanupPaths.push(dbPath); + const store = new SqliteCronStore({ dbPath }); + for (const [scheduleId, mode] of [ + ["sched-missing", undefined], + ["sched-unknown", "unknown"], + ["sched-act", "act"], + ] as const) { + store.upsertSpec({ + externalId: scheduleId, + sourcePath: `hub/schedules/${scheduleId}.cron.md`, + triggerKind: "schedule", + sourceHash: `hash-${scheduleId}`, + parseStatus: "valid", + spec: { + triggerKind: "schedule", + id: scheduleId, + title: scheduleId, + prompt: "Do the work", + workspaceRoot: "/workspace", + schedule: "0 * * * *", + enabled: true, + mode: mode as CronScheduleSpec["mode"], + source: "hub-schedule", + }, + }); + } + store.close(); + + const service = new HubScheduleService({ + dbPath, + runtimeHandlers: { + startSession: vi.fn(async () => ({ sessionId: "unused" })), + sendSession: vi.fn(async () => ({ result: { text: "unused" } })), + abortSession: vi.fn(async () => ({ applied: true })), + stopSession: vi.fn(async () => ({ applied: true })), + }, + }); + try { + expect(service.getSchedule("sched-missing")?.mode).toBe("yolo"); + expect(service.getSchedule("sched-unknown")?.mode).toBe("yolo"); + expect(service.getSchedule("sched-act")?.mode).toBe("act"); + } finally { + await service.dispose(); + } + }, + ); + sqliteIt("publishes failed schedule execution events", async () => { const dbPath = await createTempDbPath(); cleanupPaths.push(dbPath); @@ -237,7 +291,63 @@ describe("HubScheduleService", () => { expect(createdReply.ok).toBe(true); const created = createdReply.payload?.schedule as { scheduleId: string; + mode: string; }; + expect(created.mode).toBe("yolo"); + + const planReply = await commands.handleCommand({ + version: "v1", + command: "schedule.update", + payload: { scheduleId: created.scheduleId, mode: "plan" }, + }); + expect(planReply.ok).toBe(true); + expect((planReply.payload?.schedule as { mode: string }).mode).toBe( + "plan", + ); + + const omittedModeReply = await commands.handleCommand({ + version: "v1", + command: "schedule.update", + payload: { scheduleId: created.scheduleId, name: "Renamed routine" }, + }); + expect(omittedModeReply.ok).toBe(true); + expect( + (omittedModeReply.payload?.schedule as { mode: string }).mode, + ).toBe("plan"); + + for (const invalidMode of [null, "", "invalid"]) { + const invalidUpdateReply = await commands.handleCommand({ + version: "v1", + command: "schedule.update", + payload: { scheduleId: created.scheduleId, mode: invalidMode }, + }); + expect(invalidUpdateReply).toMatchObject({ + ok: false, + error: { + code: "schedule_command_failed", + message: "mode must be one of: act, plan, yolo", + }, + }); + } + + const invalidCreateReply = await commands.handleCommand({ + version: "v1", + command: "schedule.create", + payload: { + name: "Invalid routine", + cronPattern: "30 * * * *", + prompt: "Do not create", + workspaceRoot: "/workspace", + mode: "invalid", + }, + }); + expect(invalidCreateReply).toMatchObject({ + ok: false, + error: { + code: "schedule_command_failed", + message: "mode must be one of: act, plan, yolo", + }, + }); const listReply = await commands.handleCommand({ version: "v1", @@ -245,11 +355,13 @@ describe("HubScheduleService", () => { payload: { limit: 10 }, }); expect(listReply.ok).toBe(true); - expect( - (listReply.payload?.schedules as Array<{ scheduleId: string }>).some( - (item) => item.scheduleId === created.scheduleId, - ), - ).toBe(true); + const listedSchedule = ( + listReply.payload?.schedules as Array<{ + scheduleId: string; + mode: string; + }> + ).find((item) => item.scheduleId === created.scheduleId); + expect(listedSchedule).toMatchObject({ mode: "plan" }); } finally { await service.dispose(); } diff --git a/sdk/packages/core/src/cron/service/schedule-service.ts b/sdk/packages/core/src/cron/service/schedule-service.ts index 34df85e9e4..c551605ad6 100644 --- a/sdk/packages/core/src/cron/service/schedule-service.ts +++ b/sdk/packages/core/src/cron/service/schedule-service.ts @@ -139,7 +139,7 @@ function specToSchedule(spec: CronSpecRecord): ScheduleRecord { } : undefined, enabled: spec.enabled && !spec.removed && spec.parseStatus === "valid", - mode: spec.mode === "plan" ? "plan" : spec.mode === "yolo" ? "yolo" : "act", + mode: spec.mode === "plan" ? "plan" : spec.mode === "act" ? "act" : "yolo", systemPrompt: spec.systemPrompt, maxIterations: spec.maxIterations, timeoutSeconds: spec.timeoutSeconds, diff --git a/sdk/packages/core/src/cron/specs/cron-spec-parser.test.ts b/sdk/packages/core/src/cron/specs/cron-spec-parser.test.ts index 652779b61b..f8f893f3ec 100644 --- a/sdk/packages/core/src/cron/specs/cron-spec-parser.test.ts +++ b/sdk/packages/core/src/cron/specs/cron-spec-parser.test.ts @@ -52,6 +52,7 @@ describe("computeContentHash", () => { expect(r.triggerKind).toBe("one_off"); expect(r.spec?.title).toBe("Clean"); expect(r.spec?.prompt).toBe("Remove stale files."); + expect(r.spec?.mode).toBe("act"); }); it("defaults to yolo and parses cron runtime fields", () => { diff --git a/sdk/packages/core/src/cron/store/sqlite-cron-store.test.ts b/sdk/packages/core/src/cron/store/sqlite-cron-store.test.ts index 84f3c80a1b..e1f16ae134 100644 --- a/sdk/packages/core/src/cron/store/sqlite-cron-store.test.ts +++ b/sdk/packages/core/src/cron/store/sqlite-cron-store.test.ts @@ -78,6 +78,72 @@ describe("SqliteCronStore", () => { expect(result.record.source).toBe("automation"); }); + it("defaults hub schedules to yolo and preserves explicit modes on update", () => { + const created = store.createHubSchedule({ + name: "Routine", + cronPattern: "0 * * * *", + prompt: "Do the work", + workspaceRoot: "/ws", + }); + expect(created.mode).toBe("yolo"); + + const act = store.updateHubSchedule(created.externalId, { + scheduleId: created.externalId, + mode: "act", + }); + expect(act?.mode).toBe("act"); + expect( + store.updateHubSchedule(created.externalId, { + scheduleId: created.externalId, + name: "Renamed routine", + })?.mode, + ).toBe("act"); + + expect( + store.updateHubSchedule(created.externalId, { + scheduleId: created.externalId, + mode: "plan", + })?.mode, + ).toBe("plan"); + expect( + store.updateHubSchedule(created.externalId, { + scheduleId: created.externalId, + mode: "yolo", + })?.mode, + ).toBe("yolo"); + }); + + it.each([ + undefined, + "unknown", + ])("falls back to yolo when updating a hub schedule with stored mode %s", (storedMode) => { + const scheduleId = `sched-${storedMode ?? "missing"}`; + store.upsertSpec({ + externalId: scheduleId, + sourcePath: `hub/schedules/${scheduleId}.cron.md`, + triggerKind: "schedule", + sourceHash: "legacy-hash", + parseStatus: "valid", + spec: { + triggerKind: "schedule", + id: scheduleId, + title: "Legacy routine", + prompt: "Do the work", + workspaceRoot: "/ws", + schedule: "0 * * * *", + enabled: true, + mode: storedMode as CronScheduleSpec["mode"], + source: "hub-schedule", + }, + }); + + const updated = store.updateHubSchedule(scheduleId, { + scheduleId, + name: "Updated routine", + }); + expect(updated?.mode).toBe("yolo"); + }); + it("does not bump revision on cosmetic-only re-upsert with same hash", () => { store.upsertSpec({ externalId: "cleanup", diff --git a/sdk/packages/core/src/cron/store/sqlite-cron-store.ts b/sdk/packages/core/src/cron/store/sqlite-cron-store.ts index 8876bc6259..9ab239d7f4 100644 --- a/sdk/packages/core/src/cron/store/sqlite-cron-store.ts +++ b/sdk/packages/core/src/cron/store/sqlite-cron-store.ts @@ -391,7 +391,7 @@ function hubScheduleInputToCronSpec(input: HubScheduleCreateInput): CronSpec { title: input.name.trim(), prompt: input.prompt, workspaceRoot: input.workspaceRoot.trim(), - mode: input.mode ?? "act", + mode: input.mode ?? "yolo", systemPrompt: input.systemPrompt, modelSelection: input.modelSelection ? JSON.parse(JSON.stringify(input.modelSelection)) @@ -491,9 +491,9 @@ function cronSpecRecordToHubScheduleInput( updates.mode ?? (current.mode === "plan" ? "plan" - : current.mode === "yolo" - ? "yolo" - : "act"), + : current.mode === "act" + ? "act" + : "yolo"), systemPrompt: updates.systemPrompt === null ? undefined diff --git a/sdk/packages/core/src/hub/daemon/runtime-handlers.ts b/sdk/packages/core/src/hub/daemon/runtime-handlers.ts index 00e31e8fd4..d1897fd959 100644 --- a/sdk/packages/core/src/hub/daemon/runtime-handlers.ts +++ b/sdk/packages/core/src/hub/daemon/runtime-handlers.ts @@ -9,6 +9,7 @@ import type { HubScheduleRuntimeHandlers, HubScheduleServiceOptions, } from "../../cron/service/schedule-service"; +import type { VerifySubmitExecutor } from "../../extensions/tools"; import { LocalRuntimeHost } from "../../runtime/host/local-runtime-host"; import { SqliteSessionStore } from "../../services/storage/sqlite-session-store"; import { CoreSessionService } from "../../session/services/session-service"; @@ -72,8 +73,14 @@ export interface CreateLocalHubScheduleRuntimeHandlersOptions export function createLocalHubScheduleRuntimeHandlers( options: CreateLocalHubScheduleRuntimeHandlersOptions = {}, ): HubScheduleRuntimeHandlers { + const submitScheduledRun: VerifySubmitExecutor = async (summary) => summary; const sessionHost = new LocalRuntimeHost({ sessionService: new CoreSessionService(new SqliteSessionStore()), + capabilities: { + toolExecutors: { + submit: submitScheduledRun, + }, + }, fetch: options.fetch, telemetry: options.telemetry, }); diff --git a/sdk/packages/core/src/hub/server/fetch-wiring.test.ts b/sdk/packages/core/src/hub/server/fetch-wiring.test.ts index 9f91166b38..7377385a92 100644 --- a/sdk/packages/core/src/hub/server/fetch-wiring.test.ts +++ b/sdk/packages/core/src/hub/server/fetch-wiring.test.ts @@ -1,4 +1,6 @@ +import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared"; import { describe, expect, it, vi } from "vitest"; +import type { RuntimeCapabilities } from "../../runtime/capabilities/runtime-capabilities"; const localRuntimeHostMock = vi.hoisted(() => vi.fn().mockImplementation(function (this: unknown, _options: unknown) { @@ -17,7 +19,7 @@ vi.mock("../../runtime/host/local-runtime-host", () => ({ LocalRuntimeHost: localRuntimeHostMock, })); -describe("hub server fetch wiring", () => { +describe("hub runtime wiring", () => { it("forwards observability into the internal LocalRuntimeHost", async () => { localRuntimeHostMock.mockClear(); const { HubServerTransport } = (await import(".")) as unknown as { @@ -141,4 +143,73 @@ describe("hub server fetch wiring", () => { }; expect(constructorArgs.fetch).toBeUndefined(); }); + + it("provides an executable headless completion tool to the real yolo runtime builder", async () => { + localRuntimeHostMock.mockClear(); + const { createLocalHubScheduleRuntimeHandlers } = await import( + "../daemon/runtime-handlers" + ); + const { DefaultRuntimeBuilder } = await import( + "../../runtime/orchestration/runtime-builder" + ); + + createLocalHubScheduleRuntimeHandlers(); + + const constructorArgs = localRuntimeHostMock.mock.calls[0]?.[0] as { + capabilities?: RuntimeCapabilities; + }; + const toolExecutors = constructorArgs.capabilities?.toolExecutors; + expect(toolExecutors?.submit).toBeTypeOf("function"); + + const runtime = await new DefaultRuntimeBuilder().build({ + config: { + providerId: "cline", + modelId: CLINE_DEFAULT_MODEL_ID, + cwd: process.cwd(), + workspaceRoot: process.cwd(), + systemPrompt: "Run unattended.", + mode: "yolo", + enableTools: true, + enableSpawnAgent: false, + enableAgentTeams: false, + }, + toolExecutors, + toolPolicies: { + "*": { enabled: false, autoApprove: true }, + submit_and_exit: { enabled: true, autoApprove: true }, + ask_question: { enabled: false, autoApprove: true }, + }, + }); + + try { + const toolNames = runtime.tools.map((tool) => tool.name); + expect(toolNames).toContain("submit_and_exit"); + expect(toolNames).not.toContain("ask_question"); + const submitTool = runtime.tools.find( + (tool) => tool.name === "submit_and_exit", + ); + if (!submitTool) { + throw new Error("Expected submit_and_exit to be available."); + } + expect(submitTool.lifecycle).toEqual({ completesRun: true }); + expect(runtime.completionPolicy).toEqual({ + requireCompletionTool: true, + }); + await expect( + submitTool.execute( + { + summary: "Scheduled work completed successfully.", + verified: true, + }, + { + agentId: "scheduled-agent", + conversationId: "scheduled-conversation", + iteration: 1, + }, + ), + ).resolves.toBe("Scheduled work completed successfully."); + } finally { + await runtime.shutdown("test complete"); + } + }); }); diff --git a/sdk/packages/core/src/services/providers/local-provider-service.test.ts b/sdk/packages/core/src/services/providers/local-provider-service.test.ts index c9b99d6efb..86b6ab0191 100644 --- a/sdk/packages/core/src/services/providers/local-provider-service.test.ts +++ b/sdk/packages/core/src/services/providers/local-provider-service.test.ts @@ -2,6 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import * as LlmsModels from "@cline/llms"; +import { CLINE_DEFAULT_MODEL_ID } from "@cline/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearLiveModelsCatalogCache } from "../llms/provider-defaults"; import { ProviderSettingsManager } from "../storage/provider-settings-manager"; @@ -883,7 +884,7 @@ describe("models.json model overlays", () => { expect(provider).toMatchObject({ id: "cline", baseUrl: "https://api.cline.bot/api/v1", - defaultModelId: "anthropic/claude-sonnet-4.6", + defaultModelId: CLINE_DEFAULT_MODEL_ID, }); const { models } = await getLocalProviderModels("cline"); diff --git a/sdk/packages/llms/src/index.browser.ts b/sdk/packages/llms/src/index.browser.ts index 081d3c9755..65f620522e 100644 --- a/sdk/packages/llms/src/index.browser.ts +++ b/sdk/packages/llms/src/index.browser.ts @@ -1,3 +1,4 @@ +export { CLINE_DEFAULT_MODEL_ID } from "@cline/shared"; export type { ModelCollection, ModelIdAliasRule, diff --git a/sdk/packages/llms/src/index.ts b/sdk/packages/llms/src/index.ts index 677641c42b..45c45342b0 100644 --- a/sdk/packages/llms/src/index.ts +++ b/sdk/packages/llms/src/index.ts @@ -1,3 +1,4 @@ +export { CLINE_DEFAULT_MODEL_ID } from "@cline/shared"; export type { ModelCollection, ModelIdAliasRule, diff --git a/sdk/packages/llms/src/providers/builtins.test.ts b/sdk/packages/llms/src/providers/builtins.test.ts index 148b64b223..31aba7f23f 100644 --- a/sdk/packages/llms/src/providers/builtins.test.ts +++ b/sdk/packages/llms/src/providers/builtins.test.ts @@ -1,4 +1,8 @@ -import { CLINE_ENVIRONMENT_ENV, CLINE_ENVIRONMENTS } from "@cline/shared"; +import { + CLINE_DEFAULT_MODEL_ID, + CLINE_ENVIRONMENT_ENV, + CLINE_ENVIRONMENTS, +} from "@cline/shared"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { BUILTIN_SPECS } from "./builtins"; import { getModelsForProvider, getProvider } from "./model-registry"; @@ -52,6 +56,10 @@ describe("cline builtin spec defaults.baseUrl", () => { }); describe("cline builtin models", () => { + it("exposes its canonical default model ID", () => { + expect(findClineSpec().defaultModelId).toBe(CLINE_DEFAULT_MODEL_ID); + }); + it("prefers Vercel-style Z.ai model ids over equivalent OpenRouter ids", async () => { const models = await getModelsForProvider("cline"); diff --git a/sdk/packages/llms/src/providers/builtins.ts b/sdk/packages/llms/src/providers/builtins.ts index 1be6e04789..42a43ca3fb 100644 --- a/sdk/packages/llms/src/providers/builtins.ts +++ b/sdk/packages/llms/src/providers/builtins.ts @@ -1,4 +1,5 @@ import { + CLINE_DEFAULT_MODEL_ID, type GatewayModelCapability, type GatewayModelDefinition, type GatewayProviderManifest, @@ -45,7 +46,6 @@ export const DEFAULT_INTERNAL_OCA_BASE_URL = "https://code-internal.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm"; export const DEFAULT_EXTERNAL_OCA_BASE_URL = "https://code.aiservice.us-chicago-1.oci.oraclecloud.com/20250206/app/litellm"; -const CLINE_DEFAULT_MODEL_ID = "anthropic/claude-sonnet-4.6"; const CLINE_PASS_PROVIDER_ID = "cline-pass"; const OPENAI_CODEX_DEFAULT_MODEL_ID = "gpt-5.4"; const OPENROUTER_STICKY_SESSION_METADATA: GatewayProviderMetadata = { diff --git a/sdk/packages/shared/src/hub.test.ts b/sdk/packages/shared/src/hub.test.ts index fa617ebc66..a7db860ed9 100644 --- a/sdk/packages/shared/src/hub.test.ts +++ b/sdk/packages/shared/src/hub.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { isHubProtocolCompatible } from "./hub"; +import { isHubProtocolCompatible, readHubScheduleMode } from "./hub"; describe("isHubProtocolCompatible", () => { it("accepts a hub whose supported client range includes the client protocol", () => { @@ -29,3 +29,26 @@ describe("isHubProtocolCompatible", () => { }); }); }); + +describe("readHubScheduleMode", () => { + it("defaults only when mode is absent", () => { + expect(readHubScheduleMode(undefined, "yolo")).toBe("yolo"); + expect(readHubScheduleMode({}, "yolo")).toBe("yolo"); + expect(readHubScheduleMode({ mode: "plan" }, "yolo")).toBe("plan"); + }); + + it("preserves omission for schedule updates", () => { + expect(readHubScheduleMode({})).toBeUndefined(); + }); + + it.each([ + undefined, + null, + "", + "invalid", + ])("rejects a present invalid mode: %s", (mode) => { + expect(() => readHubScheduleMode({ mode }, "yolo")).toThrow( + "mode must be one of: act, plan, yolo", + ); + }); +}); diff --git a/sdk/packages/shared/src/hub.ts b/sdk/packages/shared/src/hub.ts index 093dbb1712..247935e701 100644 --- a/sdk/packages/shared/src/hub.ts +++ b/sdk/packages/shared/src/hub.ts @@ -329,6 +329,34 @@ export interface ScheduleExecutionRecord { export const ONE_TIME_SCHEDULE_CRON_PATTERN = "0"; export const ONE_TIME_SCHEDULE_RUN_AT_METADATA_KEY = "__hubScheduleRunAt"; +export const HUB_SCHEDULE_MODES = ["act", "plan", "yolo"] as const; +export type HubScheduleMode = (typeof HUB_SCHEDULE_MODES)[number]; + +export function isHubScheduleMode(value: unknown): value is HubScheduleMode { + return HUB_SCHEDULE_MODES.some((mode) => mode === value); +} + +export function readHubScheduleMode( + payload: Record | undefined, + defaultWhenAbsent: HubScheduleMode, +): HubScheduleMode; +export function readHubScheduleMode( + payload: Record | undefined, +): HubScheduleMode | undefined; +export function readHubScheduleMode( + payload: Record | undefined, + defaultWhenAbsent?: HubScheduleMode, +): HubScheduleMode | undefined { + if (!payload || !Object.hasOwn(payload, "mode")) { + return defaultWhenAbsent; + } + const mode = payload.mode; + if (isHubScheduleMode(mode)) { + return mode; + } + throw new Error(`mode must be one of: ${HUB_SCHEDULE_MODES.join(", ")}`); +} + export interface HubScheduleCreateInput { name: string; cronPattern: string; @@ -337,7 +365,7 @@ export interface HubScheduleCreateInput { cwd?: string; modelSelection?: GatewayModelSelection; enabled?: boolean; - mode?: "act" | "plan" | "yolo"; + mode?: HubScheduleMode; systemPrompt?: string; maxIterations?: number; timeoutSeconds?: number; @@ -357,7 +385,7 @@ export interface HubScheduleUpdateInput { cwd?: string; modelSelection?: GatewayModelSelection; enabled?: boolean; - mode?: "act" | "plan" | "yolo"; + mode?: HubScheduleMode; systemPrompt?: string | null; maxIterations?: number | null; timeoutSeconds?: number | null; diff --git a/sdk/packages/shared/src/index.browser.ts b/sdk/packages/shared/src/index.browser.ts index a64f97018b..3f13ca5aeb 100644 --- a/sdk/packages/shared/src/index.browser.ts +++ b/sdk/packages/shared/src/index.browser.ts @@ -258,6 +258,7 @@ export { stripModeNotices, xmlTagsRemoval, } from "./prompt/format"; +export { CLINE_DEFAULT_MODEL_ID } from "./providers/defaults"; export { isClineProvider } from "./providers/utils"; export { REMOTE_URI_SCHEME } from "./remote-config/constants"; export type { diff --git a/sdk/packages/shared/src/index.ts b/sdk/packages/shared/src/index.ts index 5547c75118..4a7bd5481c 100644 --- a/sdk/packages/shared/src/index.ts +++ b/sdk/packages/shared/src/index.ts @@ -274,6 +274,7 @@ export { stripModeNotices, xmlTagsRemoval, } from "./prompt/format"; +export { CLINE_DEFAULT_MODEL_ID } from "./providers/defaults"; export { isClineProvider } from "./providers/utils"; export { buildRemoteConfigSessionBlobUploadMetadata, diff --git a/sdk/packages/shared/src/providers/defaults.ts b/sdk/packages/shared/src/providers/defaults.ts new file mode 100644 index 0000000000..07131e52ad --- /dev/null +++ b/sdk/packages/shared/src/providers/defaults.ts @@ -0,0 +1,2 @@ +/** Canonical default model for the Cline provider. */ +export const CLINE_DEFAULT_MODEL_ID = "anthropic/claude-sonnet-5";