From facf1a5f9eaaad88d5ca9e2de358b78cebe8b945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 05:06:39 +0000 Subject: [PATCH] commanders run on Sonnet, not the account default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit every new commander came up on whatever the owner last picked in a chat — Fable, currently. that default is chosen for interactive work, and a commander is the opposite: long-running, mostly repeating one settled job, and started by a button rather than by somebody thinking about which model they want. COMMANDER_DEFAULT_MODEL, in both places, because the two ways of starting a commander do not share code — the server's /commanders/new route and the detail component's own first turn. per-surface defaults belong in settings eventually. "commanders default to Sonnet" is the decision; this is where it lives until there is a better place for it. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- src/servers/api/chat/websocket.ts | 14 +++++++++++++ src/servers/api/command-hq/router.ts | 5 +++-- .../src/apps/CommandHQ/CommandHqDetail.tsx | 20 ++++++++++++------- .../officerdev/src/apps/CommandHQ/types.ts | 10 ++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/servers/api/chat/websocket.ts b/src/servers/api/chat/websocket.ts index 911b8536..41b80af2 100644 --- a/src/servers/api/chat/websocket.ts +++ b/src/servers/api/chat/websocket.ts @@ -47,6 +47,20 @@ import { snapshotClaudeUsage } from './usage-store'; // Default model when no user preference is set export const DEFAULT_MODEL = 'claude-code'; +/** + * What a commander runs on, regardless of the account's default. + * + * A commander is long-running and mostly does the same settled thing over and over, so it is the one + * surface where the account default — chosen for interactive work — is the wrong answer. Pinned here + * rather than left to the picker so a commander created today does not depend on what the owner last + * selected for a chat. + * + * Temporary in the sense that per-surface defaults belong in settings; permanent in the sense that + * "commanders default to Sonnet" is the decision, and this is where it lives until there is a better + * place for it. + */ +export const COMMANDER_DEFAULT_MODEL = 'claude-code/sonnet'; + // Harness selection: the `claude-code` provider runs through the Claude sidecar; every other provider // (opencode/anthropic/openai/… — all `providerID/modelID` ids) runs through the OpenCode server. const isClaudeModel = (model: string): boolean => model.startsWith('claude-code'); diff --git a/src/servers/api/command-hq/router.ts b/src/servers/api/command-hq/router.ts index eed24f8c..92e7d0f5 100644 --- a/src/servers/api/command-hq/router.ts +++ b/src/servers/api/command-hq/router.ts @@ -28,7 +28,7 @@ import { type ChatIdentity, type ClaudeSessionSummary, } from '../chat/claude-sessions'; -import { collectRunningTasks, startClaudeTurn, getUserDefaultModel, DEFAULT_MODEL } from '../chat/websocket'; +import { collectRunningTasks, startClaudeTurn, COMMANDER_DEFAULT_MODEL } from '../chat/websocket'; import { listCommandHqCategories, createCommandHqCategory, @@ -373,7 +373,8 @@ commandHqRouter.post('/commanders/new', async (ctx) => { if (credentialError) console.warn(`[command-hq] ${row.dirName}: no credential minted — ${credentialError}`); const sessionId = randomUUID(); - const model = (await getUserDefaultModel(user.id)) || DEFAULT_MODEL; + // Not the account default: a commander runs on Sonnet — see COMMANDER_DEFAULT_MODEL. + const model = COMMANDER_DEFAULT_MODEL; const outcome = await new Promise<{ ok: true; transcriptId: string } | { ok: false; error: string }>((resolve) => { // 30s covers a slow spawn (member login checks, a cold sidecar) without holding an HTTP connection diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx index b1b56a27..e38f29ff 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx @@ -9,6 +9,7 @@ import { useClient } from 'hooks/useClient'; import { errorText } from 'helpers/error-text'; import type { ClaudeSessionDetail } from 'state/useClaudeSessions'; import { useChat, EmbeddableChat } from '../Chat'; +import { COMMANDER_DEFAULT_MODEL } from './types'; import { DirPickerModal } from '../ChatHistory/DirPickerModal'; import type { ChatMessage } from '../Chat/types'; import { categoryFromParam, commandHqListPath, commandHqSessionPath } from './command-hq-routes'; @@ -102,13 +103,15 @@ const startBrief = (name: string, referencePath: string): string => '', 'Read the **Commanders** skill before you reply. It is how this works, and it is short.', '', - 'You watch for the thing I name and you respond when it happens. You never cause it — not while we', - 'are setting you up, and not on a first run. If you find work of that kind already waiting here, it', - 'is mine to start, not yours to volunteer for.', + 'Go and LOOK at that path, then talk to me.', '', - 'So: go and LOOK at it first, then talk to me. How many things are in there, what kinds, how they are', - 'named, how recently they arrive. Open with what you actually found and one question about it — that', - 'is worth ten generic ones, and it tells me straight away whether you are looking at the right thing.', + 'Your first message is SHORT. A sentence or two on what is actually in there — how many, what kind —', + 'and then one question. Nothing else. Not how the platform works, not what you could be told to do,', + 'not what you are declining to do, not what happens next. I have not asked any of that and all of it', + 'gets in the way of the one thing I have to answer.', + '', + 'Plain words. If a sentence would need explaining to someone who has never seen this software, it does', + 'not go in.', '', 'The name tells you the subject. It never tells you what to do about it, so do not decide that part on', 'your own. One question, and wait.', @@ -303,7 +306,10 @@ function ActiveNewCommander({ const { t, tUi } = useTranslation(); const navigate = useNavigate(); - const chat = useChat(undefined, undefined, { + // Sonnet, not the account's default. A commander is long-running and mostly repeats one settled job, so + // whatever the owner last picked for an interactive chat is the wrong answer here. Mirrors + // COMMANDER_DEFAULT_MODEL on the server, which the button-created path uses. + const chat = useChat(undefined, COMMANDER_DEFAULT_MODEL, { replaceUrl: false, context: 'command-hq', // Known from this component's very first render — see the docstring above for why that timing is the diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/types.ts b/src/workspaces/officerdev/src/apps/CommandHQ/types.ts index c1ef23ed..67d0be45 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/types.ts +++ b/src/workspaces/officerdev/src/apps/CommandHQ/types.ts @@ -1,3 +1,13 @@ +/** + * What a commander runs on, regardless of the account's default. + * + * A commander is long-running and mostly repeats one settled job, so the account default — chosen for + * interactive work — is the wrong answer here. Mirrors `COMMANDER_DEFAULT_MODEL` in + * `api/chat/websocket.ts`, which the server uses for the button-created path; both exist because the two + * ways of starting a commander do not share code. + */ +export const COMMANDER_DEFAULT_MODEL = 'claude-code/sonnet'; + // Mirrors the JSON shapes `src/servers/api/command-hq/router.ts` actually returns — hand-kept rather // than imported from officerdb, the same way every other frontend API type in this codebase is (dates // arrive as ISO strings over the wire, not `Date`, which is the one thing importing the server's own