From f2d04ccb13eab1bf3de4648dedcdf095e587dc17 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Sun, 15 Mar 2026 17:21:06 +0000 Subject: [PATCH 1/2] feat(claude-code): add ClaudeCodeSettingsSchema with effort, thinking, and thinkingBudgetTokens --- src/backends/catalog.ts | 37 ++++++++++++ src/backends/claude-code/index.ts | 5 ++ src/backends/claude-code/settings.ts | 31 ++++++++++ src/backends/types.ts | 9 +++ src/config/engineSettings.ts | 2 + tests/unit/backends/claude-code.test.ts | 78 +++++++++++++++++++++++++ tests/unit/config/schema.test.ts | 2 +- 7 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/backends/claude-code/settings.ts diff --git a/src/backends/catalog.ts b/src/backends/catalog.ts index 78dde240..8b1a5ba3 100644 --- a/src/backends/catalog.ts +++ b/src/backends/catalog.ts @@ -37,6 +37,43 @@ export const CLAUDE_CODE_ENGINE_DEFINITION: AgentEngineDefinition = { options: CLAUDE_CODE_MODELS, }, logLabel: 'Claude Code Log', + settings: { + title: 'Claude Code Settings', + description: 'Effort level and thinking mode for Claude Code runs.', + fields: [ + { + key: 'effort', + label: 'Effort', + type: 'select', + description: 'Controls the overall effort level applied during the run.', + options: [ + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, + { value: 'max', label: 'Max' }, + ], + }, + { + key: 'thinking', + label: 'Thinking', + type: 'select', + description: 'Controls extended thinking mode.', + options: [ + { value: 'adaptive', label: 'Adaptive' }, + { value: 'enabled', label: 'Enabled' }, + { value: 'disabled', label: 'Disabled' }, + ], + }, + { + key: 'thinkingBudgetTokens', + label: 'Thinking Budget Tokens', + // TODO: Frontend 'number' field type is not yet supported (Story #2). + // The dashboard will render this field once numeric fields are implemented. + type: 'number', + description: 'Maximum tokens allocated for extended thinking (optional).', + }, + ], + }, }; export const CODEX_ENGINE_DEFINITION: AgentEngineDefinition = { diff --git a/src/backends/claude-code/index.ts b/src/backends/claude-code/index.ts index 0706c031..853020e3 100644 --- a/src/backends/claude-code/index.ts +++ b/src/backends/claude-code/index.ts @@ -27,6 +27,7 @@ import type { AgentEngine, AgentEngineResult, AgentExecutionPlan } from '../type import { buildClaudeEnv } from './env.js'; import { buildHooks } from './hooks.js'; import { CLAUDE_CODE_MODEL_IDS, DEFAULT_CLAUDE_CODE_MODEL } from './models.js'; +import { ClaudeCodeSettingsSchema } from './settings.js'; export { buildToolGuidance, buildTaskPrompt, buildSystemPrompt } from '../nativeTools.js'; export { buildClaudeEnv as buildEnv } from './env.js'; @@ -460,6 +461,10 @@ export class ClaudeCodeEngine implements AgentEngine { return resolveClaudeModel(cascadeModel); } + getSettingsSchema() { + return ClaudeCodeSettingsSchema; + } + async beforeExecute(plan: AgentExecutionPlan): Promise { // Ensure onboarding flag exists (required for both API key and subscription auth) ensureOnboardingFlag(); diff --git a/src/backends/claude-code/settings.ts b/src/backends/claude-code/settings.ts new file mode 100644 index 00000000..f8f76623 --- /dev/null +++ b/src/backends/claude-code/settings.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; +import { getEngineSettings } from '../../config/engineSettings.js'; +import type { ProjectConfig } from '../../types/index.js'; + +export const ClaudeCodeSettingsSchema = z.object({ + effort: z.enum(['low', 'medium', 'high', 'max']).optional(), + thinking: z.enum(['adaptive', 'enabled', 'disabled']).optional(), + // TODO: Frontend 'number' field type is not yet supported (Story #2). + // This field is defined here for catalog registration; the dashboard will + // render it once numeric fields are implemented. + thinkingBudgetTokens: z.number().int().positive().optional(), +}); + +export type ClaudeCodeSettings = z.infer; + +export interface ResolvedClaudeCodeSettings { + effort: NonNullable; + thinking: NonNullable; + thinkingBudgetTokens?: ClaudeCodeSettings['thinkingBudgetTokens']; +} + +export function resolveClaudeCodeSettings(project: ProjectConfig): ResolvedClaudeCodeSettings { + const claudeCode = + getEngineSettings(project.engineSettings, 'claude-code', ClaudeCodeSettingsSchema) ?? {}; + + return { + effort: claudeCode.effort ?? 'high', + thinking: claudeCode.thinking ?? 'adaptive', + thinkingBudgetTokens: claudeCode.thinkingBudgetTokens, + }; +} diff --git a/src/backends/types.ts b/src/backends/types.ts index 44ee1dca..4a6fd260 100644 --- a/src/backends/types.ts +++ b/src/backends/types.ts @@ -113,6 +113,15 @@ export type AgentEngineSettingField = label: string; type: 'boolean'; description?: string; + } + | { + key: string; + label: string; + // TODO: Frontend rendering for 'number' fields is not yet implemented (Story #2). + // The catalog definition is registered here; the dashboard will render it once + // numeric field support is added. + type: 'number'; + description?: string; }; export interface AgentEngineSettingsDefinition { diff --git a/src/config/engineSettings.ts b/src/config/engineSettings.ts index f1e06396..054ea198 100644 --- a/src/config/engineSettings.ts +++ b/src/config/engineSettings.ts @@ -1,6 +1,8 @@ import { z } from 'zod'; // Re-export schemas from engine directories for backward compatibility. +export { ClaudeCodeSettingsSchema } from '../backends/claude-code/settings.js'; +export type { ClaudeCodeSettings } from '../backends/claude-code/settings.js'; export { CodexSettingsSchema } from '../backends/codex/settings.js'; export type { CodexSettings } from '../backends/codex/settings.js'; export { OpenCodeSettingsSchema } from '../backends/opencode/settings.js'; diff --git a/tests/unit/backends/claude-code.test.ts b/tests/unit/backends/claude-code.test.ts index a17452cc..59af758e 100644 --- a/tests/unit/backends/claude-code.test.ts +++ b/tests/unit/backends/claude-code.test.ts @@ -33,6 +33,7 @@ import { CLAUDE_CODE_MODEL_IDS, DEFAULT_CLAUDE_CODE_MODEL, } from '../../../src/backends/claude-code/models.js'; +import { resolveClaudeCodeSettings } from '../../../src/backends/claude-code/settings.js'; import type { AgentExecutionPlan, ToolManifest } from '../../../src/backends/types.js'; const mockQuery = vi.mocked(query); @@ -1390,3 +1391,80 @@ describe('ClaudeCodeEngine lifecycle hooks', () => { expect(existsSync(sessionDir)).toBe(false); }); }); + +describe('resolveClaudeCodeSettings', () => { + it('returns defaults when no engine settings are configured', () => { + const project = makeInput().project; + expect(resolveClaudeCodeSettings(project)).toEqual({ + effort: 'high', + thinking: 'adaptive', + thinkingBudgetTokens: undefined, + }); + }); + + it('applies explicit effort modes from project engine settings', () => { + const project = { + ...makeInput().project, + engineSettings: { 'claude-code': { effort: 'max' } }, + } as AgentExecutionPlan['project']; + expect(resolveClaudeCodeSettings(project)).toEqual({ + effort: 'max', + thinking: 'adaptive', + thinkingBudgetTokens: undefined, + }); + + const projectLow = { + ...makeInput().project, + engineSettings: { 'claude-code': { effort: 'low' } }, + } as AgentExecutionPlan['project']; + expect(resolveClaudeCodeSettings(projectLow).effort).toBe('low'); + + const projectMedium = { + ...makeInput().project, + engineSettings: { 'claude-code': { effort: 'medium' } }, + } as AgentExecutionPlan['project']; + expect(resolveClaudeCodeSettings(projectMedium).effort).toBe('medium'); + }); + + it('applies explicit thinking modes from project engine settings', () => { + const projectEnabled = { + ...makeInput().project, + engineSettings: { 'claude-code': { thinking: 'enabled' } }, + } as AgentExecutionPlan['project']; + expect(resolveClaudeCodeSettings(projectEnabled)).toEqual({ + effort: 'high', + thinking: 'enabled', + thinkingBudgetTokens: undefined, + }); + + const projectDisabled = { + ...makeInput().project, + engineSettings: { 'claude-code': { thinking: 'disabled' } }, + } as AgentExecutionPlan['project']; + expect(resolveClaudeCodeSettings(projectDisabled).thinking).toBe('disabled'); + }); + + it('applies thinkingBudgetTokens when provided', () => { + const project = { + ...makeInput().project, + engineSettings: { 'claude-code': { thinkingBudgetTokens: 10000 } }, + } as AgentExecutionPlan['project']; + expect(resolveClaudeCodeSettings(project)).toEqual({ + effort: 'high', + thinking: 'adaptive', + thinkingBudgetTokens: 10000, + }); + }); + + it('ClaudeCodeEngine.getSettingsSchema() returns ClaudeCodeSettingsSchema', () => { + const engine = new ClaudeCodeEngine(); + const schema = engine.getSettingsSchema(); + expect(schema).toBeDefined(); + // Verify it parses valid settings + const result = schema.safeParse({ effort: 'high', thinking: 'adaptive' }); + expect(result.success).toBe(true); + // Verify it rejects invalid settings + const bad = schema.safeParse({ effort: 'ultra' }); + expect(bad.success).toBe(false); + }); +}); diff --git a/tests/unit/config/schema.test.ts b/tests/unit/config/schema.test.ts index 48585bf6..7cbbab1e 100644 --- a/tests/unit/config/schema.test.ts +++ b/tests/unit/config/schema.test.ts @@ -351,7 +351,7 @@ describe.concurrent('validateConfig', () => { repo: 'owner/repo', trello: { boardId: 'b1', lists: {}, labels: {} }, engineSettings: { - 'claude-code': { + 'unknown-engine': { foo: 'bar', }, }, From c10f2ecf1b771a7c1bbb9611002a50dfbcc0163b Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Sun, 15 Mar 2026 17:29:07 +0000 Subject: [PATCH 2/2] fix(frontend): add 'number' to EngineSettingField type for frontend build compatibility The backend catalog now includes a 'number' type field (thinkingBudgetTokens) in ClaudeCodeEngine settings. The frontend EngineSettingField union type only had 'select' and 'boolean', causing a TypeScript error during the frontend build. This adds 'number' to the frontend type union and skips rendering number fields until Story #2 implements numeric field support. Co-Authored-By: Claude Opus 4.6 --- .../components/settings/engine-settings-fields.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/src/components/settings/engine-settings-fields.tsx b/web/src/components/settings/engine-settings-fields.tsx index 0278a5fb..a3a4c31b 100644 --- a/web/src/components/settings/engine-settings-fields.tsx +++ b/web/src/components/settings/engine-settings-fields.tsx @@ -25,6 +25,15 @@ type EngineSettingField = label: string; type: 'boolean'; description?: string; + } + | { + key: string; + label: string; + // TODO: Frontend rendering for 'number' fields is not yet implemented (Story #2). + // The field type is defined here for type compatibility with the backend catalog; + // the dashboard will render it once numeric field support is added. + type: 'number'; + description?: string; }; interface EngineDefinition { @@ -97,6 +106,9 @@ export function EngineSettingsFields({
{engine.settings.fields.map((field) => { + // TODO: 'number' field rendering is not yet implemented (Story #2). + if (field.type === 'number') return null; + const rawValue = activeEngineValues[field.key]; return (