diff --git a/.gitignore b/.gitignore index 1c19aa51..736a7235 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,5 @@ data/ # GSD planning docs (local-only) .planning/ +.mcp.json +.planning diff --git a/CLAUDE.md b/CLAUDE.md index 128cd8d7..84dd1fbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,7 +161,7 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali - `haiku` → `claude-haiku-4-5` - `sonnet` → `claude-sonnet-4-20250514` -- `opus` → `claude-opus-4-5-20251101` +- `opus` → `claude-opus-4-6` ## Environment Variables diff --git a/Dockerfile b/Dockerfile index 03911b45..a68901e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -118,6 +118,7 @@ RUN curl -fsSL https://opencode.ai/install | bash && \ echo "=== Checking OpenCode CLI installation ===" && \ ls -la /home/automaker/.local/bin/ && \ (which opencode && opencode --version) || echo "opencode installed (may need auth setup)" + USER root # Add PATH to profile so it's available in all interactive shells (for login shells) @@ -147,6 +148,15 @@ COPY --from=server-builder /app/apps/server/package*.json ./apps/server/ # Copy node_modules (includes symlinks to libs) COPY --from=server-builder /app/node_modules ./node_modules +# Install Playwright Chromium browser for AI agent verification tests +# This adds ~300MB to the image but enables automated testing mode out of the box +# Using the locally installed playwright ensures we use the pinned version from package-lock.json +USER automaker +RUN ./node_modules/.bin/playwright install chromium && \ + echo "=== Playwright Chromium installed ===" && \ + ls -la /home/automaker/.cache/ms-playwright/ +USER root + # Create data and projects directories RUN mkdir -p /data /projects && chown automaker:automaker /data /projects diff --git a/README.md b/README.md index 49b14343..98f8683e 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,42 @@ services: The Docker image supports both AMD64 and ARM64 architectures. The GitHub CLI and Claude CLI are automatically downloaded for the correct architecture during build. +##### Playwright for Automated Testing + +The Docker image includes **Playwright Chromium pre-installed** for AI agent verification tests. When agents implement features in automated testing mode, they use Playwright to verify the implementation works correctly. + +**No additional setup required** - Playwright verification works out of the box. + +#### Optional: Persist browsers for manual updates + +By default, Playwright Chromium is pre-installed in the Docker image. If you need to manually update browsers or want to persist browser installations across container restarts (not image rebuilds), you can mount a volume. + +**Important:** When you first add this volume mount to an existing setup, the empty volume will override the pre-installed browsers. You must re-install them: + +```bash +# After adding the volume mount for the first time +docker exec --user automaker -w /app automaker-server npx playwright install chromium +``` + +Add this to your `docker-compose.override.yml`: + +```yaml +services: + server: + volumes: + - playwright-cache:/home/automaker/.cache/ms-playwright + +volumes: + playwright-cache: + name: automaker-playwright-cache +``` + +**Updating browsers manually:** + +```bash +docker exec --user automaker -w /app automaker-server npx playwright install chromium +``` + ### Testing #### End-to-End Tests (Playwright) diff --git a/apps/server/package.json b/apps/server/package.json index c9015aea..4a7a75a8 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,7 +24,7 @@ "test:unit": "vitest run tests/unit" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.1.76", + "@anthropic-ai/claude-agent-sdk": "0.2.32", "@automaker/dependency-resolver": "1.0.0", "@automaker/git-utils": "1.0.0", "@automaker/model-resolver": "1.0.0", @@ -34,7 +34,7 @@ "@automaker/utils": "1.0.0", "@github/copilot-sdk": "^0.1.16", "@modelcontextprotocol/sdk": "1.25.2", - "@openai/codex-sdk": "^0.77.0", + "@openai/codex-sdk": "^0.98.0", "cookie-parser": "1.4.7", "cors": "2.8.5", "dotenv": "17.2.3", @@ -45,6 +45,7 @@ "yaml": "2.7.0" }, "devDependencies": { + "@playwright/test": "1.57.0", "@types/cookie": "0.6.0", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 6c63a865..a78e9e83 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -121,21 +121,57 @@ const BOX_CONTENT_WIDTH = 67; // The Claude Agent SDK can use either ANTHROPIC_API_KEY or Claude Code CLI authentication (async () => { const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; + const hasEnvOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN; + + logger.debug('[CREDENTIAL_CHECK] Starting credential detection...'); + logger.debug('[CREDENTIAL_CHECK] Environment variables:', { + hasAnthropicKey, + hasEnvOAuthToken, + }); if (hasAnthropicKey) { logger.info('✓ ANTHROPIC_API_KEY detected'); return; } + if (hasEnvOAuthToken) { + logger.info('✓ CLAUDE_CODE_OAUTH_TOKEN detected'); + return; + } + // Check for Claude Code CLI authentication + // Store indicators outside the try block so we can use them in the warning message + let cliAuthIndicators: Awaited> | null = null; + try { - const indicators = await getClaudeAuthIndicators(); + cliAuthIndicators = await getClaudeAuthIndicators(); + const indicators = cliAuthIndicators; + + // Log detailed credential detection results + const { checks, ...indicatorSummary } = indicators; + logger.debug('[CREDENTIAL_CHECK] Claude CLI auth indicators:', indicatorSummary); + + logger.debug('[CREDENTIAL_CHECK] File check details:', checks); + const hasCliAuth = indicators.hasStatsCacheWithActivity || (indicators.hasSettingsFile && indicators.hasProjectsSessions) || (indicators.hasCredentialsFile && (indicators.credentials?.hasOAuthToken || indicators.credentials?.hasApiKey)); + logger.debug('[CREDENTIAL_CHECK] Auth determination:', { + hasCliAuth, + reason: hasCliAuth + ? indicators.hasStatsCacheWithActivity + ? 'stats cache with activity' + : indicators.hasSettingsFile && indicators.hasProjectsSessions + ? 'settings file + project sessions' + : indicators.credentials?.hasOAuthToken + ? 'credentials file with OAuth token' + : 'credentials file with API key' + : 'no valid credentials found', + }); + if (hasCliAuth) { logger.info('✓ Claude Code CLI authentication detected'); return; @@ -145,7 +181,7 @@ const BOX_CONTENT_WIDTH = 67; logger.warn('Error checking for Claude Code CLI authentication:', error); } - // No authentication found - show warning + // No authentication found - show warning with paths that were checked const wHeader = '⚠️ WARNING: No Claude authentication configured'.padEnd(BOX_CONTENT_WIDTH); const w1 = 'The Claude Agent SDK requires authentication to function.'.padEnd(BOX_CONTENT_WIDTH); const w2 = 'Options:'.padEnd(BOX_CONTENT_WIDTH); @@ -158,6 +194,33 @@ const BOX_CONTENT_WIDTH = 67; BOX_CONTENT_WIDTH ); + // Build paths checked summary from the indicators (if available) + let pathsCheckedInfo = ''; + if (cliAuthIndicators) { + const pathsChecked: string[] = []; + + // Collect paths that were checked (paths are always populated strings) + pathsChecked.push(`Settings: ${cliAuthIndicators.checks.settingsFile.path}`); + pathsChecked.push(`Stats cache: ${cliAuthIndicators.checks.statsCache.path}`); + pathsChecked.push(`Projects dir: ${cliAuthIndicators.checks.projectsDir.path}`); + for (const credFile of cliAuthIndicators.checks.credentialFiles) { + pathsChecked.push(`Credentials: ${credFile.path}`); + } + + if (pathsChecked.length > 0) { + pathsCheckedInfo = ` +║ ║ +║ ${'Paths checked:'.padEnd(BOX_CONTENT_WIDTH)}║ +${pathsChecked + .map((p) => { + const maxLen = BOX_CONTENT_WIDTH - 4; + const display = p.length > maxLen ? '...' + p.slice(-(maxLen - 3)) : p; + return `║ ${display.padEnd(maxLen)} ║`; + }) + .join('\n')}`; + } + } + logger.warn(` ╔═════════════════════════════════════════════════════════════════════╗ ║ ${wHeader}║ @@ -169,7 +232,7 @@ const BOX_CONTENT_WIDTH = 67; ║ ${w3}║ ║ ${w4}║ ║ ${w5}║ -║ ${w6}║ +║ ${w6}║${pathsCheckedInfo} ║ ║ ╚═════════════════════════════════════════════════════════════════════╝ `); diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index cc1df2f5..674350a5 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -253,11 +253,27 @@ function buildMcpOptions(config: CreateSdkOptionsConfig): McpOptions { /** * Build thinking options for SDK configuration. * Converts ThinkingLevel to maxThinkingTokens for the Claude SDK. + * For adaptive thinking (Opus 4.6), omits maxThinkingTokens to let the model + * decide its own reasoning depth. * * @param thinkingLevel - The thinking level to convert - * @returns Object with maxThinkingTokens if thinking is enabled + * @returns Object with maxThinkingTokens if thinking is enabled with a budget */ function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial { + if (!thinkingLevel || thinkingLevel === 'none') { + return {}; + } + + // Adaptive thinking (Opus 4.6): don't set maxThinkingTokens + // The model will use adaptive thinking by default + if (thinkingLevel === 'adaptive') { + logger.debug( + `buildThinkingOptions: thinkingLevel="adaptive" -> no maxThinkingTokens (model decides)` + ); + return {}; + } + + // Manual budget-based thinking for Haiku/Sonnet const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel); logger.debug( `buildThinkingOptions: thinkingLevel="${thinkingLevel}" -> maxThinkingTokens=${maxThinkingTokens}` diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index cfb59093..78a0a0c7 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -219,8 +219,11 @@ export class ClaudeProvider extends BaseProvider { // claudeCompatibleProvider takes precedence over claudeApiProfile const providerConfig = claudeCompatibleProvider || claudeApiProfile; - // Convert thinking level to token budget - const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel); + // Build thinking configuration + // Adaptive thinking (Opus 4.6): don't set maxThinkingTokens, model uses adaptive by default + // Manual thinking (Haiku/Sonnet): use budget_tokens + const maxThinkingTokens = + thinkingLevel === 'adaptive' ? undefined : getThinkingTokenBudget(thinkingLevel); // Build Claude SDK options const sdkOptions: Options = { @@ -349,13 +352,13 @@ export class ClaudeProvider extends BaseProvider { getAvailableModels(): ModelDefinition[] { const models = [ { - id: 'claude-opus-4-5-20251101', - name: 'Claude Opus 4.5', - modelString: 'claude-opus-4-5-20251101', + id: 'claude-opus-4-6', + name: 'Claude Opus 4.6', + modelString: 'claude-opus-4-6', provider: 'anthropic', - description: 'Most capable Claude model', + description: 'Most capable Claude model with adaptive thinking', contextWindow: 200000, - maxOutputTokens: 16000, + maxOutputTokens: 128000, supportsVision: true, supportsTools: true, tier: 'premium' as const, diff --git a/apps/server/src/providers/codex-models.ts b/apps/server/src/providers/codex-models.ts index 141d5355..7840888b 100644 --- a/apps/server/src/providers/codex-models.ts +++ b/apps/server/src/providers/codex-models.ts @@ -19,12 +19,11 @@ const MAX_OUTPUT_16K = 16000; export const CODEX_MODELS: ModelDefinition[] = [ // ========== Recommended Codex Models ========== { - id: CODEX_MODEL_MAP.gpt52Codex, - name: 'GPT-5.2-Codex', - modelString: CODEX_MODEL_MAP.gpt52Codex, + id: CODEX_MODEL_MAP.gpt53Codex, + name: 'GPT-5.3-Codex', + modelString: CODEX_MODEL_MAP.gpt53Codex, provider: 'openai', - description: - 'Most advanced agentic coding model for complex software engineering (default for ChatGPT users).', + description: 'Latest frontier agentic coding model.', contextWindow: CONTEXT_WINDOW_256K, maxOutputTokens: MAX_OUTPUT_32K, supportsVision: true, @@ -33,12 +32,25 @@ export const CODEX_MODELS: ModelDefinition[] = [ default: true, hasReasoning: true, }, + { + id: CODEX_MODEL_MAP.gpt52Codex, + name: 'GPT-5.2-Codex', + modelString: CODEX_MODEL_MAP.gpt52Codex, + provider: 'openai', + description: 'Frontier agentic coding model.', + contextWindow: CONTEXT_WINDOW_256K, + maxOutputTokens: MAX_OUTPUT_32K, + supportsVision: true, + supportsTools: true, + tier: 'premium' as const, + hasReasoning: true, + }, { id: CODEX_MODEL_MAP.gpt51CodexMax, name: 'GPT-5.1-Codex-Max', modelString: CODEX_MODEL_MAP.gpt51CodexMax, provider: 'openai', - description: 'Optimized for long-horizon, agentic coding tasks in Codex.', + description: 'Codex-optimized flagship for deep and fast reasoning.', contextWindow: CONTEXT_WINDOW_256K, maxOutputTokens: MAX_OUTPUT_32K, supportsVision: true, @@ -51,7 +63,7 @@ export const CODEX_MODELS: ModelDefinition[] = [ name: 'GPT-5.1-Codex-Mini', modelString: CODEX_MODEL_MAP.gpt51CodexMini, provider: 'openai', - description: 'Smaller, more cost-effective version for faster workflows.', + description: 'Optimized for codex. Cheaper, faster, but less capable.', contextWindow: CONTEXT_WINDOW_128K, maxOutputTokens: MAX_OUTPUT_16K, supportsVision: true, @@ -66,7 +78,7 @@ export const CODEX_MODELS: ModelDefinition[] = [ name: 'GPT-5.2', modelString: CODEX_MODEL_MAP.gpt52, provider: 'openai', - description: 'Best general agentic model for tasks across industries and domains.', + description: 'Latest frontier model with improvements across knowledge, reasoning and coding.', contextWindow: CONTEXT_WINDOW_256K, maxOutputTokens: MAX_OUTPUT_32K, supportsVision: true, diff --git a/apps/server/src/providers/provider-factory.ts b/apps/server/src/providers/provider-factory.ts index 1e91760f..a6dff69e 100644 --- a/apps/server/src/providers/provider-factory.ts +++ b/apps/server/src/providers/provider-factory.ts @@ -103,7 +103,7 @@ export class ProviderFactory { /** * Get the appropriate provider for a given model ID * - * @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto") + * @param modelId Model identifier (e.g., "claude-opus-4-6", "cursor-gpt-4o", "cursor-auto") * @param options Optional settings * @param options.throwOnDisconnected Throw error if provider is disconnected (default: true) * @returns Provider instance for the model diff --git a/apps/server/src/routes/setup/routes/verify-claude-auth.ts b/apps/server/src/routes/setup/routes/verify-claude-auth.ts index df04d462..7df27c3d 100644 --- a/apps/server/src/routes/setup/routes/verify-claude-auth.ts +++ b/apps/server/src/routes/setup/routes/verify-claude-auth.ts @@ -6,6 +6,7 @@ import type { Request, Response } from 'express'; import { query } from '@anthropic-ai/claude-agent-sdk'; import { createLogger } from '@automaker/utils'; +import { getClaudeAuthIndicators } from '@automaker/platform'; import { getApiKey } from '../common.js'; import { createSecureAuthEnv, @@ -320,9 +321,28 @@ export function createVerifyClaudeAuthHandler() { authMethod, }); + // Determine specific auth type for success messages + const effectiveAuthMethod = authMethod ?? 'api_key'; + let authType: 'oauth' | 'api_key' | 'cli' | undefined; + if (authenticated) { + if (effectiveAuthMethod === 'api_key') { + authType = 'api_key'; + } else if (effectiveAuthMethod === 'cli') { + // Check if CLI auth is via OAuth (Claude Code subscription) or generic CLI + try { + const indicators = await getClaudeAuthIndicators(); + authType = indicators.credentials?.hasOAuthToken ? 'oauth' : 'cli'; + } catch { + // Fall back to generic CLI if credential check fails + authType = 'cli'; + } + } + } + res.json({ success: true, authenticated, + authType, error: errorMessage || undefined, }); } catch (error) { diff --git a/apps/server/tests/unit/lib/model-resolver.test.ts b/apps/server/tests/unit/lib/model-resolver.test.ts index c1bff78d..65e3115d 100644 --- a/apps/server/tests/unit/lib/model-resolver.test.ts +++ b/apps/server/tests/unit/lib/model-resolver.test.ts @@ -35,7 +35,7 @@ describe('model-resolver.ts', () => { it("should resolve 'opus' alias to full model string", () => { const result = resolveModelString('opus'); - expect(result).toBe('claude-opus-4-5-20251101'); + expect(result).toBe('claude-opus-4-6'); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining('Migrated legacy ID: "opus" -> "claude-opus"') ); @@ -117,7 +117,7 @@ describe('model-resolver.ts', () => { describe('getEffectiveModel', () => { it('should prioritize explicit model over session and default', () => { const result = getEffectiveModel('opus', 'haiku', 'gpt-5.2'); - expect(result).toBe('claude-opus-4-5-20251101'); + expect(result).toBe('claude-opus-4-6'); }); it('should use session model when explicit is not provided', () => { diff --git a/apps/server/tests/unit/lib/sdk-options.test.ts b/apps/server/tests/unit/lib/sdk-options.test.ts index 029cd8fa..69d69794 100644 --- a/apps/server/tests/unit/lib/sdk-options.test.ts +++ b/apps/server/tests/unit/lib/sdk-options.test.ts @@ -491,5 +491,29 @@ describe('sdk-options.ts', () => { expect(options.maxThinkingTokens).toBeUndefined(); }); }); + + describe('adaptive thinking for Opus 4.6', () => { + it('should not set maxThinkingTokens for adaptive thinking (model decides)', async () => { + const { createAutoModeOptions } = await import('@/lib/sdk-options.js'); + + const options = createAutoModeOptions({ + cwd: '/test/path', + thinkingLevel: 'adaptive', + }); + + expect(options.maxThinkingTokens).toBeUndefined(); + }); + + it('should not include maxThinkingTokens when thinkingLevel is "none"', async () => { + const { createAutoModeOptions } = await import('@/lib/sdk-options.js'); + + const options = createAutoModeOptions({ + cwd: '/test/path', + thinkingLevel: 'none', + }); + + expect(options.maxThinkingTokens).toBeUndefined(); + }); + }); }); }); diff --git a/apps/server/tests/unit/providers/claude-provider.test.ts b/apps/server/tests/unit/providers/claude-provider.test.ts index c3f83f8f..7df211ef 100644 --- a/apps/server/tests/unit/providers/claude-provider.test.ts +++ b/apps/server/tests/unit/providers/claude-provider.test.ts @@ -39,7 +39,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Hello', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -59,7 +59,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test prompt', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test/dir', systemPrompt: 'You are helpful', maxTurns: 10, @@ -71,7 +71,7 @@ describe('claude-provider.ts', () => { expect(sdk.query).toHaveBeenCalledWith({ prompt: 'Test prompt', options: expect.objectContaining({ - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', systemPrompt: 'You are helpful', maxTurns: 10, cwd: '/test/dir', @@ -91,7 +91,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -116,7 +116,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', abortController, }); @@ -145,7 +145,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Current message', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', conversationHistory, sdkSessionId: 'test-session-id', @@ -176,7 +176,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: arrayPrompt as any, - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -196,7 +196,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -222,7 +222,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -286,7 +286,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -313,7 +313,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -341,7 +341,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -366,12 +366,12 @@ describe('claude-provider.ts', () => { expect(models).toHaveLength(4); }); - it('should include Claude Opus 4.5', () => { + it('should include Claude Opus 4.6', () => { const models = provider.getAvailableModels(); - const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101'); + const opus = models.find((m) => m.id === 'claude-opus-4-6'); expect(opus).toBeDefined(); - expect(opus?.name).toBe('Claude Opus 4.5'); + expect(opus?.name).toBe('Claude Opus 4.6'); expect(opus?.provider).toBe('anthropic'); }); @@ -400,7 +400,7 @@ describe('claude-provider.ts', () => { it('should mark Opus as default', () => { const models = provider.getAvailableModels(); - const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101'); + const opus = models.find((m) => m.id === 'claude-opus-4-6'); expect(opus?.default).toBe(true); }); diff --git a/apps/server/tests/unit/providers/provider-factory.test.ts b/apps/server/tests/unit/providers/provider-factory.test.ts index fbf01e90..b9aef928 100644 --- a/apps/server/tests/unit/providers/provider-factory.test.ts +++ b/apps/server/tests/unit/providers/provider-factory.test.ts @@ -54,8 +54,8 @@ describe('provider-factory.ts', () => { describe('getProviderForModel', () => { describe('Claude models (claude-* prefix)', () => { - it('should return ClaudeProvider for claude-opus-4-5-20251101', () => { - const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101'); + it('should return ClaudeProvider for claude-opus-4-6', () => { + const provider = ProviderFactory.getProviderForModel('claude-opus-4-6'); expect(provider).toBeInstanceOf(ClaudeProvider); }); @@ -70,7 +70,7 @@ describe('provider-factory.ts', () => { }); it('should be case-insensitive for claude models', () => { - const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-5-20251101'); + const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-6'); expect(provider).toBeInstanceOf(ClaudeProvider); }); }); diff --git a/apps/ui/docs/AGENT_ARCHITECTURE.md b/apps/ui/docs/AGENT_ARCHITECTURE.md index 4c9f0d11..f5c374c4 100644 --- a/apps/ui/docs/AGENT_ARCHITECTURE.md +++ b/apps/ui/docs/AGENT_ARCHITECTURE.md @@ -199,7 +199,7 @@ The agent is configured with: ```javascript { - model: "claude-opus-4-5-20251101", + model: "claude-opus-4-6", maxTurns: 20, cwd: workingDirectory, allowedTools: [ diff --git a/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx b/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx index 3a5f6d35..7b597c8c 100644 --- a/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx +++ b/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx @@ -69,6 +69,29 @@ export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialog For safer operation, consider running Automaker in Docker. See the README for instructions.

+ +
+

+ Already running in Docker? Try these troubleshooting steps: +

+
    +
  • + Ensure IS_CONTAINERIZED=true is + set in your docker-compose environment +
  • +
  • + Verify the server container has the environment variable:{' '} + + docker exec automaker-server printenv IS_CONTAINERIZED + +
  • +
  • Rebuild and restart containers if you recently changed the configuration
  • +
  • + Check the server logs for startup messages:{' '} + docker-compose logs server +
  • +
+
diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx index b8dd8776..a816204f 100644 --- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx @@ -28,7 +28,7 @@ import { cn } from '@/lib/utils'; import { modelSupportsThinking } from '@/lib/utils'; import { useAppStore, ThinkingLevel, FeatureImage, PlanningMode, Feature } from '@/store/app-store'; import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types'; -import { supportsReasoningEffort } from '@automaker/types'; +import { supportsReasoningEffort, isAdaptiveThinkingModel } from '@automaker/types'; import { PrioritySelector, WorkModeSelector, @@ -264,7 +264,20 @@ export function AddFeatureDialog({ }, [planningMode]); const handleModelChange = (entry: PhaseModelEntry) => { - setModelEntry(entry); + // Normalize thinking level when switching between adaptive and non-adaptive models + const isNewModelAdaptive = + typeof entry.model === 'string' && isAdaptiveThinkingModel(entry.model); + const currentLevel = entry.thinkingLevel || 'none'; + + if (isNewModelAdaptive && currentLevel !== 'none' && currentLevel !== 'adaptive') { + // Switching TO Opus 4.6 with a manual level -> auto-switch to 'adaptive' + setModelEntry({ ...entry, thinkingLevel: 'adaptive' }); + } else if (!isNewModelAdaptive && currentLevel === 'adaptive') { + // Switching FROM Opus 4.6 with adaptive -> auto-switch to 'high' + setModelEntry({ ...entry, thinkingLevel: 'high' }); + } else { + setModelEntry(entry); + } }; const buildFeatureData = (): FeatureData | null => { diff --git a/apps/ui/src/components/views/board-view/shared/model-constants.ts b/apps/ui/src/components/views/board-view/shared/model-constants.ts index c56ad46a..2816e556 100644 --- a/apps/ui/src/components/views/board-view/shared/model-constants.ts +++ b/apps/ui/src/components/views/board-view/shared/model-constants.ts @@ -167,7 +167,14 @@ export const ALL_MODELS: ModelOption[] = [ ...COPILOT_MODELS, ]; -export const THINKING_LEVELS: ThinkingLevel[] = ['none', 'low', 'medium', 'high', 'ultrathink']; +export const THINKING_LEVELS: ThinkingLevel[] = [ + 'none', + 'low', + 'medium', + 'high', + 'ultrathink', + 'adaptive', +]; export const THINKING_LEVEL_LABELS: Record = { none: 'None', @@ -175,6 +182,7 @@ export const THINKING_LEVEL_LABELS: Record = { medium: 'Med', high: 'High', ultrathink: 'Ultra', + adaptive: 'Adaptive', }; /** diff --git a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx index 74b791a3..3a69d587 100644 --- a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx +++ b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx @@ -2,19 +2,26 @@ import { Label } from '@/components/ui/label'; import { Brain } from 'lucide-react'; import { cn } from '@/lib/utils'; import { ThinkingLevel } from '@/store/app-store'; -import { THINKING_LEVELS, THINKING_LEVEL_LABELS } from './model-constants'; +import { THINKING_LEVEL_LABELS } from './model-constants'; +import { getThinkingLevelsForModel } from '@automaker/types'; interface ThinkingLevelSelectorProps { selectedLevel: ThinkingLevel; onLevelSelect: (level: ThinkingLevel) => void; testIdPrefix?: string; + /** Model ID is required for correct thinking level filtering. + * Without it, adaptive thinking won't be available for Opus 4.6. */ + model?: string; } export function ThinkingLevelSelector({ selectedLevel, onLevelSelect, testIdPrefix = 'thinking-level', + model, }: ThinkingLevelSelectorProps) { + const levels = getThinkingLevelsForModel(model || ''); + return (
- {THINKING_LEVELS.map((level) => ( + {levels.map((level) => (
); diff --git a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx index 20420388..0f3c7889 100644 --- a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx +++ b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx @@ -21,6 +21,7 @@ import { isGroupSelected, getSelectedVariant, codexModelHasThinking, + getThinkingLevelsForModel, } from '@automaker/types'; import { CLAUDE_MODELS, @@ -28,7 +29,6 @@ import { OPENCODE_MODELS, GEMINI_MODELS, COPILOT_MODELS, - THINKING_LEVELS, THINKING_LEVEL_LABELS, REASONING_EFFORT_LEVELS, REASONING_EFFORT_LABELS, @@ -1296,7 +1296,9 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => ( + {getThinkingLevelsForModel( + model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || '' + ).map((level) => (
{isSelected && currentThinking === level && ( @@ -1402,7 +1405,9 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => ( + {getThinkingLevelsForModel( + model.mapsToClaudeModel === 'opus' ? 'claude-opus' : model.id || '' + ).map((level) => (