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>
This commit is contained in:
Bee
2026-07-23 14:53:03 -07:00
committed by GitHub
parent 79589c8417
commit 469debdb30
33 changed files with 543 additions and 72 deletions
+4 -2
View File
@@ -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;
+5 -3
View File
@@ -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 <n>", "Max parallel executions", "1")
.option("--metadata-json <json>", "Metadata as JSON object")
.option("--mode <act|plan>", "Execution mode")
.option("--model <model>", "Model to use", "openai/gpt-5.3-codex")
.option("--mode <act|plan|yolo>", "Execution mode", "yolo")
.option("--model <model>", "Model to use", CLINE_DEFAULT_MODEL_ID)
.option("--provider <id>", "Provider ID", "cline")
.option("--system-prompt <text>", "System prompt override")
.option("--tags <list>", "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,
@@ -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<string, unknown>): {
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 <n>", "New max parallel executions")
.option("--metadata-json <json>", "New metadata as JSON object")
.option("--mode <act|plan>", "New execution mode")
.option("--mode <act|plan|yolo>", "New execution mode")
.option("--model <model>", "New model")
.option("--name <name>", "New name")
.option("--pause", "Pause the schedule")
+5 -3
View File
@@ -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<void> {
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<void> {
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,
+7 -8
View File
@@ -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<string, unknown>,
@@ -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:
+1
View File
@@ -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",
@@ -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<string, string[]> = {
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,
@@ -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<string, unknown>,
@@ -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:
@@ -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<string, string[]> = {
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<string, string[]> = {
};
const FALLBACK_PROVIDER_REASONING_MODELS: Record<string, string[]> = {
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"],
@@ -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<string, string[]> = {
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
@@ -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,
@@ -138,7 +138,9 @@ function makeBaseConfig(overrides: Partial<CoreSessionConfig> = {}): 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", () => {
+1
View File
@@ -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",
@@ -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() {
@@ -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<ChatStartSessionRequest["toolPolicies"]> = {
"*": { enabled: false, autoApprove: true },
};
for (const tool of spec.tools) {
): NonNullable<ChatStartSessionRequest["toolPolicies"]> {
const policies: NonNullable<ChatStartSessionRequest["toolPolicies"]> =
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,
@@ -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<string, unknown>,
): 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<string, unknown>,
): 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 }),
};
}
}
@@ -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();
}
@@ -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,
@@ -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", () => {
@@ -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",
@@ -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
@@ -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,
});
@@ -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");
}
});
});
@@ -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");
+1
View File
@@ -1,3 +1,4 @@
export { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
export type {
ModelCollection,
ModelIdAliasRule,
+1
View File
@@ -1,3 +1,4 @@
export { CLINE_DEFAULT_MODEL_ID } from "@cline/shared";
export type {
ModelCollection,
ModelIdAliasRule,
@@ -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");
+1 -1
View File
@@ -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 = {
+24 -1
View File
@@ -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",
);
});
});
+30 -2
View File
@@ -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<string, unknown> | undefined,
defaultWhenAbsent: HubScheduleMode,
): HubScheduleMode;
export function readHubScheduleMode(
payload: Record<string, unknown> | undefined,
): HubScheduleMode | undefined;
export function readHubScheduleMode(
payload: Record<string, unknown> | 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;
+1
View File
@@ -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 {
+1
View File
@@ -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,
@@ -0,0 +1,2 @@
/** Canonical default model for the Cline provider. */
export const CLINE_DEFAULT_MODEL_ID = "anthropic/claude-sonnet-5";