From a9403651d464fed075f0f6dc6f47972f039e7f46 Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 00:58:32 +0100 Subject: [PATCH 01/40] fix: handle pipeline resume edge cases and improve robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes several edge cases when resuming features stuck in pipeline steps: - Detect if feature is stuck in a pipeline step during resume - Handle case where context file is missing (restart from beginning) - Handle case where pipeline step no longer exists in config - Add dedicated resumePipelineFeature() method for pipeline-specific resume logic - Add detectPipelineStatus() to extract and validate pipeline step information - Add resumeFromPipelineStep() to resume from a specific pipeline step index - Update board view to check context availability for features with pipeline status Edge cases handled: 1. No context file → restart entire pipeline from beginning 2. Step no longer exists in config → complete feature without pipeline 3. Valid step exists → resume from the crashed step 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/services/auto-mode-service.ts | 351 ++++++++++++++++++ .../board-view/hooks/use-board-effects.ts | 5 +- package-lock.json | 91 ++--- 3 files changed, 388 insertions(+), 59 deletions(-) diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index ad1b3efa..9269d5a1 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -700,6 +700,25 @@ Complete the pipeline step instructions above. Review the previous work and appl throw new Error('already running'); } + // Load feature to check status + const feature = await this.loadFeature(projectPath, featureId); + if (!feature) { + throw new Error(`Feature ${featureId} not found`); + } + + // Check if feature is stuck in a pipeline step + const pipelineInfo = await this.detectPipelineStatus( + projectPath, + featureId, + feature.status || '' + ); + + if (pipelineInfo.isPipeline) { + // Feature stuck in pipeline - use pipeline resume + return this.resumePipelineFeature(projectPath, featureId, useWorktrees, pipelineInfo); + } + + // Normal resume flow for non-pipeline features // Check if context exists in .automaker directory const featureDir = getFeatureDir(projectPath, featureId); const contextPath = path.join(featureDir, 'agent-output.md'); @@ -724,6 +743,239 @@ Complete the pipeline step instructions above. Review the previous work and appl return this.executeFeature(projectPath, featureId, useWorktrees, false); } + /** + * Resume a feature that crashed during pipeline execution + * Handles edge cases: no context, missing step, deleted pipeline step + * @param pipelineInfo - Information about the pipeline status from detectPipelineStatus() + */ + private async resumePipelineFeature( + projectPath: string, + featureId: string, + useWorktrees: boolean, + pipelineInfo: { + isPipeline: boolean; + stepId: string | null; + stepIndex: number; + totalSteps: number; + step: PipelineStep | null; + config: PipelineConfig | null; + } + ): Promise { + console.log( + `[AutoMode] Resuming feature ${featureId} from pipeline step ${pipelineInfo.stepId}` + ); + + // Check for context file + const featureDir = getFeatureDir(projectPath, featureId); + const contextPath = path.join(featureDir, 'agent-output.md'); + + let hasContext = false; + try { + await secureFs.access(contextPath); + hasContext = true; + } catch { + // No context + } + + // Edge Case 1: No context file - restart entire pipeline from beginning + if (!hasContext) { + console.warn( + `[AutoMode] No context found for pipeline feature ${featureId}, restarting from beginning` + ); + + // Reset status to in_progress and start fresh + await this.updateFeatureStatus(projectPath, featureId, 'in_progress'); + + // Remove temporary entry + this.runningFeatures.delete(featureId); + + return this.executeFeature(projectPath, featureId, useWorktrees, false); + } + + // Edge Case 2: Step no longer exists in pipeline config + if (pipelineInfo.stepIndex === -1) { + console.warn( + `[AutoMode] Step ${pipelineInfo.stepId} no longer exists in pipeline, completing feature without pipeline` + ); + + const feature = await this.loadFeature(projectPath, featureId); + const finalStatus = feature?.skipTests ? 'waiting_approval' : 'verified'; + + await this.updateFeatureStatus(projectPath, featureId, finalStatus); + + this.emitAutoModeEvent('auto_mode_feature_complete', { + featureId, + passes: true, + message: + 'Pipeline step no longer exists - feature completed without remaining pipeline steps', + projectPath, + }); + + // Remove temporary entry + this.runningFeatures.delete(featureId); + return; + } + + // Normal case: Valid pipeline step exists, has context + // Resume from the stuck step (re-execute the step that crashed) + if (!pipelineInfo.config) { + throw new Error('Pipeline config is null but stepIndex is valid - this should not happen'); + } + + return this.resumeFromPipelineStep( + projectPath, + featureId, + useWorktrees, + pipelineInfo.stepIndex, + pipelineInfo.config + ); + } + + /** + * Resume pipeline execution from a specific step index + * Re-executes the step that crashed, then continues with remaining steps + * @param pipelineConfig - Pipeline config passed from detectPipelineStatus to avoid re-reading + */ + private async resumeFromPipelineStep( + projectPath: string, + featureId: string, + useWorktrees: boolean, + startFromStepIndex: number, + pipelineConfig: PipelineConfig + ): Promise { + // Load feature and validate + const feature = await this.loadFeature(projectPath, featureId); + if (!feature) { + throw new Error(`Feature ${featureId} not found`); + } + + const sortedSteps = [...pipelineConfig.steps].sort((a, b) => a.order - b.order); + + // Validate step index + if (startFromStepIndex < 0 || startFromStepIndex >= sortedSteps.length) { + throw new Error(`Invalid step index: ${startFromStepIndex}`); + } + + // Get steps to execute (from startFromStepIndex onwards) + const stepsToExecute = sortedSteps.slice(startFromStepIndex); + + console.log( + `[AutoMode] Resuming pipeline for feature ${featureId} from step ${startFromStepIndex + 1}/${sortedSteps.length}` + ); + + // Add to running features immediately + const abortController = new AbortController(); + this.runningFeatures.set(featureId, { + featureId, + projectPath, + worktreePath: null, // Will be set below + branchName: feature.branchName ?? null, + abortController, + isAutoMode: false, + startTime: Date.now(), + }); + + try { + // Validate project path + validateWorkingDirectory(projectPath); + + // Derive workDir from feature.branchName + let worktreePath: string | null = null; + const branchName = feature.branchName; + + if (useWorktrees && branchName) { + worktreePath = await this.findExistingWorktreeForBranch(projectPath, branchName); + if (worktreePath) { + console.log(`[AutoMode] Using worktree for branch "${branchName}": ${worktreePath}`); + } else { + console.warn( + `[AutoMode] Worktree for branch "${branchName}" not found, using project path` + ); + } + } + + const workDir = worktreePath ? path.resolve(worktreePath) : path.resolve(projectPath); + validateWorkingDirectory(workDir); + + // Update running feature with worktree info + const runningFeature = this.runningFeatures.get(featureId); + if (runningFeature) { + runningFeature.worktreePath = worktreePath; + runningFeature.branchName = branchName ?? null; + } + + // Emit resume event + this.emitAutoModeEvent('auto_mode_feature_start', { + featureId, + projectPath, + feature: { + id: featureId, + title: feature.title || 'Resuming Pipeline', + description: feature.description, + }, + }); + + this.emitAutoModeEvent('auto_mode_progress', { + featureId, + content: `Resuming from pipeline step ${startFromStepIndex + 1}/${sortedSteps.length}`, + projectPath, + }); + + // Load autoLoadClaudeMd setting + const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( + projectPath, + this.settingsService, + '[AutoMode]' + ); + + // Execute remaining pipeline steps (starting from crashed step) + await this.executePipelineSteps( + projectPath, + featureId, + feature, + stepsToExecute, + workDir, + abortController, + autoLoadClaudeMd + ); + + // Determine final status + const finalStatus = feature.skipTests ? 'waiting_approval' : 'verified'; + await this.updateFeatureStatus(projectPath, featureId, finalStatus); + + console.log('[AutoMode] Pipeline resume completed successfully'); + + this.emitAutoModeEvent('auto_mode_feature_complete', { + featureId, + passes: true, + message: 'Pipeline resumed and completed successfully', + projectPath, + }); + } catch (error) { + const errorInfo = classifyError(error); + + if (errorInfo.isAbort) { + this.emitAutoModeEvent('auto_mode_feature_complete', { + featureId, + passes: false, + message: 'Pipeline resume stopped by user', + projectPath, + }); + } else { + console.error(`[AutoMode] Pipeline resume failed for feature ${featureId}:`, error); + await this.updateFeatureStatus(projectPath, featureId, 'backlog'); + this.emitAutoModeEvent('auto_mode_error', { + featureId, + error: errorInfo.message, + errorType: errorInfo.type, + projectPath, + }); + } + } finally { + this.runningFeatures.delete(featureId); + } + } + /** * Follow up on a feature with additional instructions */ @@ -2504,6 +2756,105 @@ Review the previous work and continue the implementation. If the feature appears }); } + /** + * Detect if a feature is stuck in a pipeline step and extract step info + * @returns Pipeline information including step details and config + */ + private async detectPipelineStatus( + projectPath: string, + featureId: string, + currentStatus: string + ): Promise<{ + isPipeline: boolean; + stepId: string | null; + stepIndex: number; + totalSteps: number; + step: PipelineStep | null; + config: PipelineConfig | null; + }> { + // Check if status is pipeline format using PipelineService + const isPipeline = pipelineService.isPipelineStatus(currentStatus); + + if (!isPipeline) { + return { + isPipeline: false, + stepId: null, + stepIndex: -1, + totalSteps: 0, + step: null, + config: null, + }; + } + + // Extract step ID using PipelineService + const stepId = pipelineService.getStepIdFromStatus(currentStatus); + + if (!stepId) { + console.warn( + `[AutoMode] Feature ${featureId} has invalid pipeline status format: ${currentStatus}` + ); + return { + isPipeline: true, + stepId: null, + stepIndex: -1, + totalSteps: 0, + step: null, + config: null, + }; + } + + // Load pipeline config + const config = await pipelineService.getPipelineConfig(projectPath); + + if (!config || config.steps.length === 0) { + // Pipeline config doesn't exist or empty - feature stuck with invalid pipeline status + console.warn( + `[AutoMode] Feature ${featureId} has pipeline status but no pipeline config exists` + ); + return { + isPipeline: true, + stepId, + stepIndex: -1, + totalSteps: 0, + step: null, + config: null, + }; + } + + // Find the step using PipelineService + const step = await pipelineService.getStep(projectPath, stepId); + const sortedSteps = [...config.steps].sort((a, b) => a.order - b.order); + const stepIndex = sortedSteps.findIndex((s) => s.id === stepId); + + if (stepIndex === -1 || !step) { + // Step not found in current config - step was deleted/changed + console.warn( + `[AutoMode] Feature ${featureId} stuck in step ${stepId} which no longer exists in pipeline config` + ); + return { + isPipeline: true, + stepId, + stepIndex: -1, + totalSteps: sortedSteps.length, + step: null, + config, + }; + } + + console.log( + `[AutoMode] Detected pipeline status for feature ${featureId}: step ${stepIndex + 1}/${sortedSteps.length} (${step.name})` + ); + + return { + isPipeline: true, + stepId, + stepIndex, + totalSteps: sortedSteps.length, + step, + config, + }; + } + /** * Build a focused prompt for executing a single task. * Each task gets minimal context to keep the agent focused. diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts b/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts index fbe76344..6dde9c08 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts @@ -123,7 +123,10 @@ export function useBoardEffects({ const checkAllContexts = async () => { const featuresWithPotentialContext = features.filter( (f) => - f.status === 'in_progress' || f.status === 'waiting_approval' || f.status === 'verified' + f.status === 'in_progress' || + f.status === 'waiting_approval' || + f.status === 'verified' || + (typeof f.status === 'string' && f.status.startsWith('pipeline_')) ); const contextChecks = await Promise.all( featuresWithPotentialContext.map(async (f) => ({ diff --git a/package-lock.json b/package-lock.json index d8190a03..81d55416 100644 --- a/package-lock.json +++ b/package-lock.json @@ -455,6 +455,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1038,6 +1039,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1080,6 +1082,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1900,7 +1903,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1922,7 +1924,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1939,7 +1940,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1954,7 +1954,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2722,7 +2721,6 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -2847,7 +2845,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2864,7 +2861,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2881,7 +2877,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2990,7 +2985,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3013,7 +3007,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3036,7 +3029,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3122,7 +3114,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.7.0" }, @@ -3145,7 +3136,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3165,7 +3155,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3565,8 +3554,7 @@ "version": "16.0.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "16.0.10", @@ -3580,7 +3568,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3597,7 +3584,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3614,7 +3600,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3631,7 +3616,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3648,7 +3632,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3665,7 +3648,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3682,7 +3664,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3699,7 +3680,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3790,6 +3770,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -5230,7 +5211,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -5564,6 +5544,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz", "integrity": "sha512-qWFxi2D6eGc1L03RzUuhyEOplZ7Q6q62YOl7Of9Y0q4YjwQwxRm4zxwDVtvUIoy4RLVCpqp5UoE+Nxv2PY9trg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-store": "^0.8.0", @@ -5990,6 +5971,7 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -6132,6 +6114,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6142,6 +6125,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6247,6 +6231,7 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -6740,7 +6725,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xyflow/react": { "version": "12.10.0", @@ -6838,6 +6824,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6898,6 +6885,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7496,6 +7484,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8027,8 +8016,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -8333,8 +8321,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-env": { "version": "10.1.0", @@ -8431,6 +8418,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8732,6 +8720,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -9058,7 +9047,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -9079,7 +9067,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -9330,6 +9317,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9644,6 +9632,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11311,7 +11300,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11373,7 +11361,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -13801,7 +13788,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -13818,7 +13804,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -13836,7 +13821,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -14025,6 +14009,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14034,6 +14019,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14392,7 +14378,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14581,6 +14566,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz", "integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -14629,7 +14615,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -14680,7 +14665,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14703,7 +14687,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14726,7 +14709,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14743,7 +14725,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14760,7 +14741,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14777,7 +14757,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14794,7 +14773,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14811,7 +14789,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14828,7 +14805,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14845,7 +14821,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14868,7 +14843,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14891,7 +14865,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14914,7 +14887,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14937,7 +14909,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14960,7 +14931,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15429,7 +15399,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -15599,7 +15568,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -15663,7 +15631,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -15761,6 +15728,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15965,6 +15933,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16336,6 +16305,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16425,7 +16395,8 @@ "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", @@ -16451,6 +16422,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16493,6 +16465,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -16750,6 +16723,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -16818,6 +16792,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From c587947de6a18d7662c80495bed1717a5ca8f453 Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 12:42:04 +0100 Subject: [PATCH 02/40] refactor: optimize feature loading in pipeline resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce redundant file reads by loading the feature object once and passing it down the call chain instead of reloading it multiple times. Changes: - Pass feature object to resumePipelineFeature() instead of featureId - Pass feature object to resumeFromPipelineStep() instead of featureId - Remove redundant loadFeature() calls from these methods - Add FeatureStatusWithPipeline import for type safety This improves performance by eliminating unnecessary file I/O operations and makes the data flow clearer. Co-authored-by: gemini-code-assist bot 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/services/auto-mode-service.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 9269d5a1..cab06923 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -10,7 +10,13 @@ */ import { ProviderFactory } from '../providers/provider-factory.js'; -import type { ExecuteOptions, Feature, PipelineConfig, PipelineStep } from '@automaker/types'; +import type { + ExecuteOptions, + Feature, + FeatureStatusWithPipeline, + PipelineConfig, + PipelineStep, +} from '@automaker/types'; import { buildPromptWithImages, isAbortError, @@ -710,12 +716,12 @@ Complete the pipeline step instructions above. Review the previous work and appl const pipelineInfo = await this.detectPipelineStatus( projectPath, featureId, - feature.status || '' + (feature.status || '') as FeatureStatusWithPipeline ); if (pipelineInfo.isPipeline) { // Feature stuck in pipeline - use pipeline resume - return this.resumePipelineFeature(projectPath, featureId, useWorktrees, pipelineInfo); + return this.resumePipelineFeature(projectPath, feature, useWorktrees, pipelineInfo); } // Normal resume flow for non-pipeline features @@ -746,11 +752,12 @@ Complete the pipeline step instructions above. Review the previous work and appl /** * Resume a feature that crashed during pipeline execution * Handles edge cases: no context, missing step, deleted pipeline step + * @param feature - The feature object (already loaded to avoid redundant reads) * @param pipelineInfo - Information about the pipeline status from detectPipelineStatus() */ private async resumePipelineFeature( projectPath: string, - featureId: string, + feature: Feature, useWorktrees: boolean, pipelineInfo: { isPipeline: boolean; @@ -761,6 +768,7 @@ Complete the pipeline step instructions above. Review the previous work and appl config: PipelineConfig | null; } ): Promise { + const featureId = feature.id; console.log( `[AutoMode] Resuming feature ${featureId} from pipeline step ${pipelineInfo.stepId}` ); @@ -798,8 +806,7 @@ Complete the pipeline step instructions above. Review the previous work and appl `[AutoMode] Step ${pipelineInfo.stepId} no longer exists in pipeline, completing feature without pipeline` ); - const feature = await this.loadFeature(projectPath, featureId); - const finalStatus = feature?.skipTests ? 'waiting_approval' : 'verified'; + const finalStatus = feature.skipTests ? 'waiting_approval' : 'verified'; await this.updateFeatureStatus(projectPath, featureId, finalStatus); @@ -824,7 +831,7 @@ Complete the pipeline step instructions above. Review the previous work and appl return this.resumeFromPipelineStep( projectPath, - featureId, + feature, useWorktrees, pipelineInfo.stepIndex, pipelineInfo.config @@ -834,20 +841,17 @@ Complete the pipeline step instructions above. Review the previous work and appl /** * Resume pipeline execution from a specific step index * Re-executes the step that crashed, then continues with remaining steps + * @param feature - The feature object (already loaded to avoid redundant reads) * @param pipelineConfig - Pipeline config passed from detectPipelineStatus to avoid re-reading */ private async resumeFromPipelineStep( projectPath: string, - featureId: string, + feature: Feature, useWorktrees: boolean, startFromStepIndex: number, pipelineConfig: PipelineConfig ): Promise { - // Load feature and validate - const feature = await this.loadFeature(projectPath, featureId); - if (!feature) { - throw new Error(`Feature ${featureId} not found`); - } + const featureId = feature.id; const sortedSteps = [...pipelineConfig.steps].sort((a, b) => a.order - b.order); @@ -2763,7 +2767,7 @@ Review the previous work and continue the implementation. If the feature appears private async detectPipelineStatus( projectPath: string, featureId: string, - currentStatus: string + currentStatus: FeatureStatusWithPipeline ): Promise<{ isPipeline: boolean; stepId: string | null; From 3ff9658723cccc589713de24930b94ba8893bbbd Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 12:44:19 +0100 Subject: [PATCH 03/40] refactor: remove unnecessary runningFeatures.delete() calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove confusing and unnecessary delete calls from resumeFeature() and resumePipelineFeature() methods. These were leftovers from a previous implementation where temporary entries were added to runningFeatures. The resumeFeature() method already ensures the feature is not running at the start (via has() check that throws if already running), so these delete calls serve no purpose and only add confusion about state management. Removed delete calls from: - resumeFeature() non-pipeline flow (line 748) - resumePipelineFeature() no-context case (line 798) - resumePipelineFeature() step-not-found case (line 822) Co-authored-by: gemini-code-assist bot 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/services/auto-mode-service.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index cab06923..805cf642 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -744,8 +744,6 @@ Complete the pipeline step instructions above. Review the previous work and appl } // No context, start fresh - executeFeature will handle adding to runningFeatures - // Remove the temporary entry we added - this.runningFeatures.delete(featureId); return this.executeFeature(projectPath, featureId, useWorktrees, false); } @@ -794,9 +792,6 @@ Complete the pipeline step instructions above. Review the previous work and appl // Reset status to in_progress and start fresh await this.updateFeatureStatus(projectPath, featureId, 'in_progress'); - // Remove temporary entry - this.runningFeatures.delete(featureId); - return this.executeFeature(projectPath, featureId, useWorktrees, false); } @@ -818,8 +813,6 @@ Complete the pipeline step instructions above. Review the previous work and appl projectPath, }); - // Remove temporary entry - this.runningFeatures.delete(featureId); return; } From 7a2a3ef50065e215b37d95cdda47e66acf7ed445 Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 13:02:43 +0100 Subject: [PATCH 04/40] refactor: create PipelineStatusInfo interface to eliminate type duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define a dedicated PipelineStatusInfo interface and use it consistently in both resumePipelineFeature() parameter and detectPipelineStatus() return type. This eliminates duplicate inline type definitions and improves maintainability by ensuring both locations always stay in sync. Any future changes to the pipeline status structure only need to be made in one place. Changes: - Add PipelineStatusInfo interface definition - Replace inline type in resumePipelineFeature() with PipelineStatusInfo - Replace inline return type in detectPipelineStatus() with PipelineStatusInfo Co-authored-by: gemini-code-assist bot 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/services/auto-mode-service.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 805cf642..12b2620e 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -74,6 +74,18 @@ interface PlanSpec { tasks?: ParsedTask[]; } +/** + * Information about pipeline status when resuming a feature + */ +interface PipelineStatusInfo { + isPipeline: boolean; + stepId: string | null; + stepIndex: number; + totalSteps: number; + step: PipelineStep | null; + config: PipelineConfig | null; +} + /** * Parse tasks from generated spec content * Looks for the ```tasks code block and extracts task lines @@ -757,14 +769,7 @@ Complete the pipeline step instructions above. Review the previous work and appl projectPath: string, feature: Feature, useWorktrees: boolean, - pipelineInfo: { - isPipeline: boolean; - stepId: string | null; - stepIndex: number; - totalSteps: number; - step: PipelineStep | null; - config: PipelineConfig | null; - } + pipelineInfo: PipelineStatusInfo ): Promise { const featureId = feature.id; console.log( @@ -2761,14 +2766,7 @@ Review the previous work and continue the implementation. If the feature appears projectPath: string, featureId: string, currentStatus: FeatureStatusWithPipeline - ): Promise<{ - isPipeline: boolean; - stepId: string | null; - stepIndex: number; - totalSteps: number; - step: PipelineStep | null; - config: PipelineConfig | null; - }> { + ): Promise { // Check if status is pipeline format using PipelineService const isPipeline = pipelineService.isPipelineStatus(currentStatus); From 2d309f6833d75e226487edcfa8c733f1910ceb3a Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 13:04:01 +0100 Subject: [PATCH 05/40] perf: eliminate redundant file read in detectPipelineStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unnecessary call to pipelineService.getStep() which was causing a redundant file read of pipeline.json. The config is already loaded at line 2807, so we can find the step directly from the in-memory config. Changes: - Sort config.steps first - Find stepIndex using findIndex() - Get step directly from sortedSteps[stepIndex] instead of calling getStep() - Simplify null check to only check !step instead of stepIndex === -1 || !step This optimization reduces file I/O operations and improves performance when resuming pipeline features. Co-authored-by: gemini-code-assist bot 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/services/auto-mode-service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 12b2620e..5460b02c 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -2816,12 +2816,12 @@ Review the previous work and continue the implementation. If the feature appears }; } - // Find the step using PipelineService - const step = await pipelineService.getStep(projectPath, stepId); + // Find the step directly from config (already loaded, avoid redundant file read) const sortedSteps = [...config.steps].sort((a, b) => a.order - b.order); const stepIndex = sortedSteps.findIndex((s) => s.id === stepId); + const step = stepIndex === -1 ? null : sortedSteps[stepIndex]; - if (stepIndex === -1 || !step) { + if (!step) { // Step not found in current config - step was deleted/changed console.warn( `[AutoMode] Feature ${featureId} stuck in step ${stepId} which no longer exists in pipeline config` From 2a87d55519a3ae4ba12d70e282e0bbd4a06313db Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 13:22:11 +0100 Subject: [PATCH 06/40] fix: show Resume button for features stuck in pipeline steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable the Resume button to appear for features with pipeline status (e.g., 'pipeline_step_xyz') in addition to 'in_progress' status. Previously, features that crashed during pipeline execution would show a 'testing' status badge but no Resume button, making it impossible to resume them from the UI. Changes: - Update card-actions.tsx condition to include pipeline_ status check - Resume button now shows for both in_progress and pipeline_step_* statuses - Maintains all existing behavior for other feature states This fixes the UX issue where users could see a feature was stuck in a pipeline step but had no way to resume it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../components/kanban-card/card-actions.tsx | 166 +++++++++--------- 1 file changed, 84 insertions(+), 82 deletions(-) diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx index df0d8707..762d5a66 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx @@ -109,88 +109,90 @@ export function CardActions({ )} )} - {!isCurrentAutoTask && feature.status === 'in_progress' && ( - <> - {/* Approve Plan button - shows when plan is generated and waiting for approval */} - {feature.planSpec?.status === 'generated' && onApprovePlan && ( - - )} - {feature.skipTests && onManualVerify ? ( - - ) : hasContext && onResume ? ( - - ) : onVerify ? ( - - ) : null} - {onViewOutput && !feature.skipTests && ( - - )} - - )} + {!isCurrentAutoTask && + (feature.status === 'in_progress' || + (typeof feature.status === 'string' && feature.status.startsWith('pipeline_'))) && ( + <> + {/* Approve Plan button - shows when plan is generated and waiting for approval */} + {feature.planSpec?.status === 'generated' && onApprovePlan && ( + + )} + {feature.skipTests && onManualVerify ? ( + + ) : hasContext && onResume ? ( + + ) : onVerify ? ( + + ) : null} + {onViewOutput && !feature.skipTests && ( + + )} + + )} {!isCurrentAutoTask && feature.status === 'verified' && ( <> {/* Logs button */} From 71e03c2a13762ad5f8d46b1818037702a94d1fb5 Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 13:55:05 +0100 Subject: [PATCH 07/40] fix: restore Verify button fallback with honest labeling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-add the onVerify fallback for in_progress/pipeline features without context, but fix the misleading UX issue where the button said 'Resume' but executed verification (tests/build). Changes: - Restore onVerify fallback as 3rd option after skipTests Verify and Resume - Change button label from 'Resume' to 'Verify' (honest!) - Change icon from PlayCircle to CheckCircle2 (matches action) - Keep same green styling for consistency This makes sense because if a feature is in_progress but has no context, it likely completed execution but the context was deleted. User should be able to verify it (run tests/build) rather than having no action available. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../components/kanban-card/card-actions.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx index 9cfcaee7..9753ba7c 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/card-actions.tsx @@ -160,6 +160,21 @@ export function CardActions({ Resume + ) : onVerify ? ( + ) : null} {onViewOutput && !feature.skipTests && ( + + +

Change default model and planning settings for new features

+
+ +
diff --git a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx index 9912201d..ae7d655b 100644 --- a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx @@ -21,7 +21,8 @@ import { FeatureTextFilePath as DescriptionTextFilePath, ImagePreviewMap, } from '@/components/ui/description-image-dropzone'; -import { GitBranch, Cpu, FolderKanban } from 'lucide-react'; +import { GitBranch, Cpu, FolderKanban, Settings2 } from 'lucide-react'; +import { useNavigate } from '@tanstack/react-router'; import { toast } from 'sonner'; import { cn, modelSupportsThinking } from '@/lib/utils'; import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store'; @@ -86,6 +87,7 @@ export function EditFeatureDialog({ isMaximized, allFeatures, }: EditFeatureDialogProps) { + const navigate = useNavigate(); const [editingFeature, setEditingFeature] = useState(feature); // Derive initial workMode from feature's branchName const [workMode, setWorkMode] = useState(() => { @@ -363,9 +365,31 @@ export function EditFeatureDialog({ {/* AI & Execution Section */}
-
- - AI & Execution +
+
+ + AI & Execution +
+ + + + + + +

Change default model and planning settings for new features

+
+
+
diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx index aa6a8a84..2655e8a5 100644 --- a/apps/ui/src/components/views/settings-view.tsx +++ b/apps/ui/src/components/views/settings-view.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; +import { useSearch } from '@tanstack/react-router'; import { useAppStore } from '@/store/app-store'; -import { useSetupStore } from '@/store/setup-store'; import { useSettingsView, type SettingsViewId } from './settings-view/hooks'; import { NAV_ITEMS } from './settings-view/config/navigation'; @@ -51,6 +51,8 @@ export function SettingsView() { setDefaultPlanningMode, defaultRequirePlanApproval, setDefaultRequirePlanApproval, + defaultFeatureModel, + setDefaultFeatureModel, autoLoadClaudeMd, setAutoLoadClaudeMd, promptCustomization, @@ -86,8 +88,11 @@ export function SettingsView() { } }; + // Get initial view from URL search params + const { view: initialView } = useSearch({ from: '/settings' }); + // Use settings view navigation hook - const { activeView, navigateTo } = useSettingsView(); + const { activeView, navigateTo } = useSettingsView({ initialView }); // Handle navigation - if navigating to 'providers', default to 'claude-provider' const handleNavigate = (viewId: SettingsViewId) => { @@ -152,11 +157,13 @@ export function SettingsView() { skipVerificationInAutoMode={skipVerificationInAutoMode} defaultPlanningMode={defaultPlanningMode} defaultRequirePlanApproval={defaultRequirePlanApproval} + defaultFeatureModel={defaultFeatureModel} onDefaultSkipTestsChange={setDefaultSkipTests} onEnableDependencyBlockingChange={setEnableDependencyBlocking} onSkipVerificationInAutoModeChange={setSkipVerificationInAutoMode} onDefaultPlanningModeChange={setDefaultPlanningMode} onDefaultRequirePlanApprovalChange={setDefaultRequirePlanApproval} + onDefaultFeatureModelChange={setDefaultFeatureModel} /> ); case 'worktrees': diff --git a/apps/ui/src/components/views/settings-view/config/navigation.ts b/apps/ui/src/components/views/settings-view/config/navigation.ts index f63d0494..6a810973 100644 --- a/apps/ui/src/components/views/settings-view/config/navigation.ts +++ b/apps/ui/src/components/views/settings-view/config/navigation.ts @@ -37,8 +37,8 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [ { label: 'Model & Prompts', items: [ - { id: 'model-defaults', label: 'Model Defaults', icon: Workflow }, { id: 'defaults', label: 'Feature Defaults', icon: FlaskConical }, + { id: 'model-defaults', label: 'Model Defaults', icon: Workflow }, { id: 'worktrees', label: 'Worktrees', icon: GitBranch }, { id: 'prompts', label: 'Prompt Customization', icon: MessageSquareText }, { id: 'api-keys', label: 'API Keys', icon: Key }, diff --git a/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx b/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx index c3b4e9ae..956d28fa 100644 --- a/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx +++ b/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx @@ -10,6 +10,7 @@ import { ScrollText, ShieldCheck, FastForward, + Cpu, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { @@ -19,6 +20,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import type { PhaseModelEntry } from '@automaker/types'; +import { PhaseModelSelector } from '../model-defaults/phase-model-selector'; type PlanningMode = 'skip' | 'lite' | 'spec' | 'full'; @@ -28,11 +31,13 @@ interface FeatureDefaultsSectionProps { skipVerificationInAutoMode: boolean; defaultPlanningMode: PlanningMode; defaultRequirePlanApproval: boolean; + defaultFeatureModel: PhaseModelEntry; onDefaultSkipTestsChange: (value: boolean) => void; onEnableDependencyBlockingChange: (value: boolean) => void; onSkipVerificationInAutoModeChange: (value: boolean) => void; onDefaultPlanningModeChange: (value: PlanningMode) => void; onDefaultRequirePlanApprovalChange: (value: boolean) => void; + onDefaultFeatureModelChange: (value: PhaseModelEntry) => void; } export function FeatureDefaultsSection({ @@ -41,11 +46,13 @@ export function FeatureDefaultsSection({ skipVerificationInAutoMode, defaultPlanningMode, defaultRequirePlanApproval, + defaultFeatureModel, onDefaultSkipTestsChange, onEnableDependencyBlockingChange, onSkipVerificationInAutoModeChange, onDefaultPlanningModeChange, onDefaultRequirePlanApprovalChange, + onDefaultFeatureModelChange, }: FeatureDefaultsSectionProps) { return (
+ {/* Default Feature Model Setting */} +
+
+ +
+
+
+ + +
+

+ The default AI model and thinking level used when creating new feature cards. +

+
+
+ + {/* Separator */} +
+ {/* Planning Mode Default */}
-
)} {/* Separator */} - {defaultPlanningMode === 'skip' &&
} +
{/* Automated Testing Setting */}
diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index bb86c10c..e4375cbf 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -562,6 +562,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void { useWorktrees: settings.useWorktrees ?? true, defaultPlanningMode: settings.defaultPlanningMode ?? 'skip', defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false, + defaultFeatureModel: settings.defaultFeatureModel ?? { model: 'opus' }, muteDoneSound: settings.muteDoneSound ?? false, enhancementModel: settings.enhancementModel ?? 'sonnet', validationModel: settings.validationModel ?? 'opus', diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 41ef6693..1fb9dbd0 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -42,6 +42,7 @@ const SETTINGS_FIELDS_TO_SYNC = [ 'useWorktrees', 'defaultPlanningMode', 'defaultRequirePlanApproval', + 'defaultFeatureModel', 'muteDoneSound', 'enhancementModel', 'validationModel', @@ -466,6 +467,7 @@ export async function refreshSettingsFromServer(): Promise { useWorktrees: serverSettings.useWorktrees, defaultPlanningMode: serverSettings.defaultPlanningMode, defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval, + defaultFeatureModel: serverSettings.defaultFeatureModel ?? { model: 'opus' }, muteDoneSound: serverSettings.muteDoneSound, enhancementModel: serverSettings.enhancementModel, validationModel: serverSettings.validationModel, diff --git a/apps/ui/src/routes/settings.tsx b/apps/ui/src/routes/settings.tsx index 74170d94..c509e93a 100644 --- a/apps/ui/src/routes/settings.tsx +++ b/apps/ui/src/routes/settings.tsx @@ -1,6 +1,16 @@ import { createFileRoute } from '@tanstack/react-router'; import { SettingsView } from '@/components/views/settings-view'; +import type { SettingsViewId } from '@/components/views/settings-view/hooks'; + +interface SettingsSearchParams { + view?: SettingsViewId; +} export const Route = createFileRoute('/settings')({ component: SettingsView, + validateSearch: (search: Record): SettingsSearchParams => { + return { + view: search.view as SettingsViewId | undefined, + }; + }, }); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 36aec5ed..280ba7c1 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -657,6 +657,7 @@ export interface AppState { defaultPlanningMode: PlanningMode; defaultRequirePlanApproval: boolean; + defaultFeatureModel: PhaseModelEntry; // Plan Approval State // When a plan requires user approval, this holds the pending approval details @@ -1104,6 +1105,7 @@ export interface AppActions { setDefaultPlanningMode: (mode: PlanningMode) => void; setDefaultRequirePlanApproval: (require: boolean) => void; + setDefaultFeatureModel: (entry: PhaseModelEntry) => void; // Plan Approval actions setPendingPlanApproval: ( @@ -1277,6 +1279,7 @@ const initialState: AppState = { specCreatingForProject: null, defaultPlanningMode: 'skip' as PlanningMode, defaultRequirePlanApproval: false, + defaultFeatureModel: { model: 'opus' } as PhaseModelEntry, pendingPlanApproval: null, claudeRefreshInterval: 60, claudeUsage: null, @@ -3093,6 +3096,7 @@ export const useAppStore = create()((set, get) => ({ setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }), setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }), + setDefaultFeatureModel: (entry) => set({ defaultFeatureModel: entry }), // Plan Approval actions setPendingPlanApproval: (approval) => set({ pendingPlanApproval: approval }), diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 38402c24..a4efa469 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -375,6 +375,8 @@ export interface GlobalSettings { defaultPlanningMode: PlanningMode; /** Default: require manual approval before generating */ defaultRequirePlanApproval: boolean; + /** Default model and thinking level for new feature cards */ + defaultFeatureModel: PhaseModelEntry; // Audio Preferences /** Mute completion notification sound */ @@ -698,6 +700,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { useWorktrees: true, defaultPlanningMode: 'skip', defaultRequirePlanApproval: false, + defaultFeatureModel: { model: 'opus' }, muteDoneSound: false, phaseModels: DEFAULT_PHASE_MODELS, enhancementModel: 'sonnet', From 33ae860059995bbd823147089ce9c8d34967c161 Mon Sep 17 00:00:00 2001 From: Soham Dasgupta Date: Tue, 13 Jan 2026 20:01:22 +0530 Subject: [PATCH 24/40] feat: update Docker volumes for OpenCode CLI data and user configuration --- docker-compose.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 69d2ddd2..97526b5f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,10 @@ services: # Persist OpenCode CLI configuration and authentication across container restarts # This allows 'opencode auth login' authentication to persist between restarts - - automaker-opencode-config:/home/automaker/.local/share/opencode + - automaker-opencode-data:/home/automaker/.local/share/opencode + + # Persist OpenCode user configuration across container restarts + - automaker-opencode-config:/home/automaker/.config/opencode # NO host directory mounts - container cannot access your laptop files # If you need to work on a project, create it INSIDE the container @@ -106,7 +109,12 @@ volumes: # Named volume for Cursor CLI configuration and authentication # Persists cursor-agent login authentication across container restarts + automaker-opencode-data: + name: automaker-opencode-data + # Named volume for OpenCode CLI data and authentication (~/.local/share/opencode) + # Persists opencode auth login authentication across container restarts + automaker-opencode-config: name: automaker-opencode-config - # Named volume for OpenCode CLI configuration and authentication - # Persists opencode auth login authentication across container restarts + # Named volume for OpenCode user configuration (~/.config/opencode) + # Persists user configuration across container restarts From bb710ada1a432e37cfbf9e9bd535fdecdc67a9fc Mon Sep 17 00:00:00 2001 From: webdevcody Date: Tue, 13 Jan 2026 09:30:15 -0500 Subject: [PATCH 25/40] feat: enhance settings view and feature defaults management - Introduced default feature model settings in the settings view, allowing users to specify the default AI model for new feature cards. - Updated navigation to include a direct link to model defaults in the settings menu. - Enhanced the Add Feature dialog to utilize the default feature model from the app store. - Implemented synchronization of the default feature model in settings migration and sync hooks. - Improved UI components to reflect changes in default settings, ensuring a cohesive user experience. --- .../board-view/dialogs/add-feature-dialog.tsx | 42 +++++++++++++++---- .../dialogs/edit-feature-dialog.tsx | 32 ++++++++++++-- .../ui/src/components/views/settings-view.tsx | 11 ++++- .../views/settings-view/config/navigation.ts | 2 +- .../feature-defaults-section.tsx | 34 ++++++++++++++- apps/ui/src/hooks/use-settings-migration.ts | 1 + apps/ui/src/hooks/use-settings-sync.ts | 2 + apps/ui/src/routes/settings.tsx | 10 +++++ apps/ui/src/store/app-store.ts | 4 ++ libs/types/src/settings.ts | 3 ++ 10 files changed, 124 insertions(+), 17 deletions(-) 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 736f3c40..dfee5c30 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 @@ -21,7 +21,8 @@ import { FeatureTextFilePath as DescriptionTextFilePath, ImagePreviewMap, } from '@/components/ui/description-image-dropzone'; -import { Play, Cpu, FolderKanban } from 'lucide-react'; +import { Play, Cpu, FolderKanban, Settings2 } from 'lucide-react'; +import { useNavigate } from '@tanstack/react-router'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { modelSupportsThinking } from '@/lib/utils'; @@ -33,7 +34,7 @@ import { PlanningMode, Feature, } from '@/store/app-store'; -import type { ReasoningEffort, PhaseModelEntry } from '@automaker/types'; +import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types'; import { supportsReasoningEffort, isClaudeModel } from '@automaker/types'; import { TestingTabContent, @@ -152,6 +153,7 @@ export function AddFeatureDialog({ forceCurrentBranchMode, }: AddFeatureDialogProps) { const isSpawnMode = !!parentFeature; + const navigate = useNavigate(); const [workMode, setWorkMode] = useState('current'); // Form state @@ -187,7 +189,8 @@ export function AddFeatureDialog({ const [selectedAncestorIds, setSelectedAncestorIds] = useState>(new Set()); // Get defaults from store - const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees } = useAppStore(); + const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } = + useAppStore(); // Track previous open state to detect when dialog opens const wasOpenRef = useRef(false); @@ -207,7 +210,7 @@ export function AddFeatureDialog({ ); setPlanningMode(defaultPlanningMode); setRequirePlanApproval(defaultRequirePlanApproval); - setModelEntry({ model: 'opus' }); + setModelEntry(defaultFeatureModel); // Initialize description history (empty for new feature) setDescriptionHistory([]); @@ -228,6 +231,7 @@ export function AddFeatureDialog({ defaultBranch, defaultPlanningMode, defaultRequirePlanApproval, + defaultFeatureModel, useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode, @@ -318,7 +322,7 @@ export function AddFeatureDialog({ // When a non-main worktree is selected, use its branch name for custom mode setBranchName(selectedNonMainWorktreeBranch || ''); setPriority(2); - setModelEntry({ model: 'opus' }); + setModelEntry(defaultFeatureModel); setWorkMode( getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode) ); @@ -473,9 +477,31 @@ export function AddFeatureDialog({ {/* AI & Execution Section */}
-
- - AI & Execution +
+
+ + AI & Execution +
+ + + + + + +

Change default model and planning settings for new features

+
+
+
diff --git a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx index 9912201d..ae7d655b 100644 --- a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx @@ -21,7 +21,8 @@ import { FeatureTextFilePath as DescriptionTextFilePath, ImagePreviewMap, } from '@/components/ui/description-image-dropzone'; -import { GitBranch, Cpu, FolderKanban } from 'lucide-react'; +import { GitBranch, Cpu, FolderKanban, Settings2 } from 'lucide-react'; +import { useNavigate } from '@tanstack/react-router'; import { toast } from 'sonner'; import { cn, modelSupportsThinking } from '@/lib/utils'; import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store'; @@ -86,6 +87,7 @@ export function EditFeatureDialog({ isMaximized, allFeatures, }: EditFeatureDialogProps) { + const navigate = useNavigate(); const [editingFeature, setEditingFeature] = useState(feature); // Derive initial workMode from feature's branchName const [workMode, setWorkMode] = useState(() => { @@ -363,9 +365,31 @@ export function EditFeatureDialog({ {/* AI & Execution Section */}
-
- - AI & Execution +
+
+ + AI & Execution +
+ + + + + + +

Change default model and planning settings for new features

+
+
+
diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx index aa6a8a84..2655e8a5 100644 --- a/apps/ui/src/components/views/settings-view.tsx +++ b/apps/ui/src/components/views/settings-view.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; +import { useSearch } from '@tanstack/react-router'; import { useAppStore } from '@/store/app-store'; -import { useSetupStore } from '@/store/setup-store'; import { useSettingsView, type SettingsViewId } from './settings-view/hooks'; import { NAV_ITEMS } from './settings-view/config/navigation'; @@ -51,6 +51,8 @@ export function SettingsView() { setDefaultPlanningMode, defaultRequirePlanApproval, setDefaultRequirePlanApproval, + defaultFeatureModel, + setDefaultFeatureModel, autoLoadClaudeMd, setAutoLoadClaudeMd, promptCustomization, @@ -86,8 +88,11 @@ export function SettingsView() { } }; + // Get initial view from URL search params + const { view: initialView } = useSearch({ from: '/settings' }); + // Use settings view navigation hook - const { activeView, navigateTo } = useSettingsView(); + const { activeView, navigateTo } = useSettingsView({ initialView }); // Handle navigation - if navigating to 'providers', default to 'claude-provider' const handleNavigate = (viewId: SettingsViewId) => { @@ -152,11 +157,13 @@ export function SettingsView() { skipVerificationInAutoMode={skipVerificationInAutoMode} defaultPlanningMode={defaultPlanningMode} defaultRequirePlanApproval={defaultRequirePlanApproval} + defaultFeatureModel={defaultFeatureModel} onDefaultSkipTestsChange={setDefaultSkipTests} onEnableDependencyBlockingChange={setEnableDependencyBlocking} onSkipVerificationInAutoModeChange={setSkipVerificationInAutoMode} onDefaultPlanningModeChange={setDefaultPlanningMode} onDefaultRequirePlanApprovalChange={setDefaultRequirePlanApproval} + onDefaultFeatureModelChange={setDefaultFeatureModel} /> ); case 'worktrees': diff --git a/apps/ui/src/components/views/settings-view/config/navigation.ts b/apps/ui/src/components/views/settings-view/config/navigation.ts index f63d0494..6a810973 100644 --- a/apps/ui/src/components/views/settings-view/config/navigation.ts +++ b/apps/ui/src/components/views/settings-view/config/navigation.ts @@ -37,8 +37,8 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [ { label: 'Model & Prompts', items: [ - { id: 'model-defaults', label: 'Model Defaults', icon: Workflow }, { id: 'defaults', label: 'Feature Defaults', icon: FlaskConical }, + { id: 'model-defaults', label: 'Model Defaults', icon: Workflow }, { id: 'worktrees', label: 'Worktrees', icon: GitBranch }, { id: 'prompts', label: 'Prompt Customization', icon: MessageSquareText }, { id: 'api-keys', label: 'API Keys', icon: Key }, diff --git a/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx b/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx index c3b4e9ae..956d28fa 100644 --- a/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx +++ b/apps/ui/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx @@ -10,6 +10,7 @@ import { ScrollText, ShieldCheck, FastForward, + Cpu, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { @@ -19,6 +20,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import type { PhaseModelEntry } from '@automaker/types'; +import { PhaseModelSelector } from '../model-defaults/phase-model-selector'; type PlanningMode = 'skip' | 'lite' | 'spec' | 'full'; @@ -28,11 +31,13 @@ interface FeatureDefaultsSectionProps { skipVerificationInAutoMode: boolean; defaultPlanningMode: PlanningMode; defaultRequirePlanApproval: boolean; + defaultFeatureModel: PhaseModelEntry; onDefaultSkipTestsChange: (value: boolean) => void; onEnableDependencyBlockingChange: (value: boolean) => void; onSkipVerificationInAutoModeChange: (value: boolean) => void; onDefaultPlanningModeChange: (value: PlanningMode) => void; onDefaultRequirePlanApprovalChange: (value: boolean) => void; + onDefaultFeatureModelChange: (value: PhaseModelEntry) => void; } export function FeatureDefaultsSection({ @@ -41,11 +46,13 @@ export function FeatureDefaultsSection({ skipVerificationInAutoMode, defaultPlanningMode, defaultRequirePlanApproval, + defaultFeatureModel, onDefaultSkipTestsChange, onEnableDependencyBlockingChange, onSkipVerificationInAutoModeChange, onDefaultPlanningModeChange, onDefaultRequirePlanApprovalChange, + onDefaultFeatureModelChange, }: FeatureDefaultsSectionProps) { return (
+ {/* Default Feature Model Setting */} +
+
+ +
+
+
+ + +
+

+ The default AI model and thinking level used when creating new feature cards. +

+
+
+ + {/* Separator */} +
+ {/* Planning Mode Default */}
-
)} {/* Separator */} - {defaultPlanningMode === 'skip' &&
} +
{/* Automated Testing Setting */}
diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index bb86c10c..e4375cbf 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -562,6 +562,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void { useWorktrees: settings.useWorktrees ?? true, defaultPlanningMode: settings.defaultPlanningMode ?? 'skip', defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false, + defaultFeatureModel: settings.defaultFeatureModel ?? { model: 'opus' }, muteDoneSound: settings.muteDoneSound ?? false, enhancementModel: settings.enhancementModel ?? 'sonnet', validationModel: settings.validationModel ?? 'opus', diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 41ef6693..1fb9dbd0 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -42,6 +42,7 @@ const SETTINGS_FIELDS_TO_SYNC = [ 'useWorktrees', 'defaultPlanningMode', 'defaultRequirePlanApproval', + 'defaultFeatureModel', 'muteDoneSound', 'enhancementModel', 'validationModel', @@ -466,6 +467,7 @@ export async function refreshSettingsFromServer(): Promise { useWorktrees: serverSettings.useWorktrees, defaultPlanningMode: serverSettings.defaultPlanningMode, defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval, + defaultFeatureModel: serverSettings.defaultFeatureModel ?? { model: 'opus' }, muteDoneSound: serverSettings.muteDoneSound, enhancementModel: serverSettings.enhancementModel, validationModel: serverSettings.validationModel, diff --git a/apps/ui/src/routes/settings.tsx b/apps/ui/src/routes/settings.tsx index 74170d94..c509e93a 100644 --- a/apps/ui/src/routes/settings.tsx +++ b/apps/ui/src/routes/settings.tsx @@ -1,6 +1,16 @@ import { createFileRoute } from '@tanstack/react-router'; import { SettingsView } from '@/components/views/settings-view'; +import type { SettingsViewId } from '@/components/views/settings-view/hooks'; + +interface SettingsSearchParams { + view?: SettingsViewId; +} export const Route = createFileRoute('/settings')({ component: SettingsView, + validateSearch: (search: Record): SettingsSearchParams => { + return { + view: search.view as SettingsViewId | undefined, + }; + }, }); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 36aec5ed..280ba7c1 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -657,6 +657,7 @@ export interface AppState { defaultPlanningMode: PlanningMode; defaultRequirePlanApproval: boolean; + defaultFeatureModel: PhaseModelEntry; // Plan Approval State // When a plan requires user approval, this holds the pending approval details @@ -1104,6 +1105,7 @@ export interface AppActions { setDefaultPlanningMode: (mode: PlanningMode) => void; setDefaultRequirePlanApproval: (require: boolean) => void; + setDefaultFeatureModel: (entry: PhaseModelEntry) => void; // Plan Approval actions setPendingPlanApproval: ( @@ -1277,6 +1279,7 @@ const initialState: AppState = { specCreatingForProject: null, defaultPlanningMode: 'skip' as PlanningMode, defaultRequirePlanApproval: false, + defaultFeatureModel: { model: 'opus' } as PhaseModelEntry, pendingPlanApproval: null, claudeRefreshInterval: 60, claudeUsage: null, @@ -3093,6 +3096,7 @@ export const useAppStore = create()((set, get) => ({ setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }), setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }), + setDefaultFeatureModel: (entry) => set({ defaultFeatureModel: entry }), // Plan Approval actions setPendingPlanApproval: (approval) => set({ pendingPlanApproval: approval }), diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 38402c24..a4efa469 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -375,6 +375,8 @@ export interface GlobalSettings { defaultPlanningMode: PlanningMode; /** Default: require manual approval before generating */ defaultRequirePlanApproval: boolean; + /** Default model and thinking level for new feature cards */ + defaultFeatureModel: PhaseModelEntry; // Audio Preferences /** Mute completion notification sound */ @@ -698,6 +700,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { useWorktrees: true, defaultPlanningMode: 'skip', defaultRequirePlanApproval: false, + defaultFeatureModel: { model: 'opus' }, muteDoneSound: false, phaseModels: DEFAULT_PHASE_MODELS, enhancementModel: 'sonnet', From 24a6633322b5f1ee3b60b42e8e1b886f61b678e3 Mon Sep 17 00:00:00 2001 From: Soham Dasgupta Date: Tue, 13 Jan 2026 20:45:33 +0530 Subject: [PATCH 26/40] fix: add OpenCode cache volume for version file persistence OpenCode stores a version file in ~/.cache/opencode/ which was causing EACCES permission errors. This adds: - Volume mount for ~/.cache/opencode - Entrypoint script to set correct ownership/permissions on the cache directory --- docker-compose.yml | 8 ++++++++ docker-entrypoint.sh | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3a7e17ed..2c4cb71e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -90,6 +90,9 @@ services: # Persist OpenCode user configuration across container restarts - automaker-opencode-config:/home/automaker/.config/opencode + # Persist OpenCode cache directory (contains version file and other cache data) + - automaker-opencode-cache:/home/automaker/.cache/opencode + # NO host directory mounts - container cannot access your laptop files # If you need to work on a project, create it INSIDE the container # or use a separate docker-compose override file @@ -123,3 +126,8 @@ volumes: name: automaker-opencode-config # Named volume for OpenCode user configuration (~/.config/opencode) # Persists user configuration across container restarts + + automaker-opencode-cache: + name: automaker-opencode-cache + # Named volume for OpenCode cache directory (~/.cache/opencode) + # Contains version file and other cached data diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 9321ce37..153f5122 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -40,6 +40,13 @@ fi chown -R automaker:automaker /home/automaker/.config/opencode chmod -R 700 /home/automaker/.config/opencode +# OpenCode also uses ~/.cache/opencode for cache data (version file, etc.) +if [ ! -d "/home/automaker/.cache/opencode" ]; then + mkdir -p /home/automaker/.cache/opencode +fi +chown -R automaker:automaker /home/automaker/.cache/opencode +chmod -R 700 /home/automaker/.cache/opencode + # If CURSOR_AUTH_TOKEN is set, write it to the cursor auth file # On Linux, cursor-agent uses ~/.config/cursor/auth.json for file-based credential storage # The env var CURSOR_AUTH_TOKEN is also checked directly by cursor-agent From ff5915dd2071a21514f3b3ff7e9c5259770b2eb3 Mon Sep 17 00:00:00 2001 From: webdevcody Date: Tue, 13 Jan 2026 12:19:24 -0500 Subject: [PATCH 27/40] refactor: update terminology in board view components - Renamed "Worktrees" to "Worktree Bar" in the BoardHeader component for clarity. - Updated comments and labels in AddFeatureDialog, PlanSettingsDialog, and WorktreeSettingsDialog to reflect the new terminology and improve user understanding of worktree mode functionality. --- apps/ui/src/components/views/board-view/board-header.tsx | 2 +- .../views/board-view/dialogs/add-feature-dialog.tsx | 2 +- .../views/board-view/dialogs/plan-settings-dialog.tsx | 6 +++--- .../views/board-view/dialogs/worktree-settings-dialog.tsx | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ui/src/components/views/board-view/board-header.tsx b/apps/ui/src/components/views/board-view/board-header.tsx index cfaa8a27..2f6e4b5e 100644 --- a/apps/ui/src/components/views/board-view/board-header.tsx +++ b/apps/ui/src/components/views/board-view/board-header.tsx @@ -138,7 +138,7 @@ export function BoardHeader({
- Use selected worktree branch + Default to worktree mode

- When enabled, features created via the Plan dialog will be assigned to the currently - selected worktree branch. When disabled, features will be added to the main branch. + Planned features will automatically use isolated worktrees, keeping changes separate + from your main branch until you're ready to merge.

diff --git a/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx index da7cb134..c742e631 100644 --- a/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/worktree-settings-dialog.tsx @@ -45,7 +45,7 @@ export function WorktreeSettingsDialog({ className="text-sm font-medium cursor-pointer flex items-center gap-2" > - Use selected worktree branch + Default to worktree mode

- When enabled, the Add Feature dialog will default to custom branch mode with the - currently selected worktree branch pre-filled. + New features will automatically use isolated worktrees, keeping changes separate + from your main branch until you're ready to merge.

From 3bd8626d488a214992346dfdc9db4ad5c7eb9b09 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:27:22 +0100 Subject: [PATCH 28/40] feat: add branch/worktree support to mass edit dialog Implement worktree creation and branch assignment in the mass edit dialog to match the functionality of the add-feature and edit-feature dialogs. Changes: - Add WorkModeSelector to mass-edit-dialog.tsx with three modes: - 'Current Branch': Work on current branch (no worktree) - 'Auto Worktree': Auto-generate branch name and create worktree - 'Custom Branch': Use specified branch name and create worktree - Update handleBulkUpdate in board-view.tsx to: - Accept workMode parameter - Create worktrees for 'auto' and 'custom' modes - Auto-select created worktrees in the board header - Handle branch name generation for 'auto' mode - Add necessary props to MassEditDialog (branchSuggestions, branchCardCounts, currentBranch) Users can now bulk-assign features to a branch and automatically create/select worktrees, enabling efficient project setup with many features. Fixes #459 Co-Authored-By: Claude Sonnet 4.5 --- apps/ui/src/components/views/board-view.tsx | 107 +++++++++++++++++- .../board-view/dialogs/mass-edit-dialog.tsx | 65 ++++++++++- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 30cd4db3..58136ebc 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -492,18 +492,104 @@ export function BoardView() { // Handler for bulk updating multiple features const handleBulkUpdate = useCallback( - async (updates: Partial) => { + async (updates: Partial, workMode: 'current' | 'auto' | 'custom') => { if (!currentProject || selectedFeatureIds.size === 0) return; try { + // Determine final branch name based on work mode: + // - 'current': No branch name, work on current branch (no worktree) + // - 'auto': Auto-generate branch name based on current branch + // - 'custom': Use the provided branch name + let finalBranchName: string | undefined; + + if (workMode === 'current') { + // No worktree isolation - work directly on current branch + finalBranchName = undefined; + } else if (workMode === 'auto') { + // Auto-generate a branch name based on current branch and timestamp + const baseBranch = currentWorktreeBranch || 'main'; + const timestamp = Date.now(); + const randomSuffix = Math.random().toString(36).substring(2, 6); + finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`; + } else { + // Custom mode - use provided branch name + finalBranchName = updates.branchName || undefined; + } + + // Create worktree for 'auto' or 'custom' modes when we have a branch name + if ((workMode === 'auto' || workMode === 'custom') && finalBranchName) { + try { + const electronApi = getElectronAPI(); + if (electronApi?.worktree?.create) { + const result = await electronApi.worktree.create( + currentProject.path, + finalBranchName + ); + if (result.success && result.worktree) { + logger.info( + `Worktree for branch "${finalBranchName}" ${ + result.worktree?.isNew ? 'created' : 'already exists' + }` + ); + // Auto-select the worktree when creating/using it for bulk update + const currentWorktrees = getWorktrees(currentProject.path); + const existingWorktree = currentWorktrees.find( + (w) => w.branch === result.worktree.branch + ); + + // Only add if it doesn't already exist (to avoid duplicates) + if (!existingWorktree) { + const newWorktreeInfo = { + path: result.worktree.path, + branch: result.worktree.branch, + isMain: false, + isCurrent: false, + hasWorktree: true, + }; + setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); + } + // Select the worktree (whether it existed or was just added) + setCurrentWorktree( + currentProject.path, + result.worktree.path, + result.worktree.branch + ); + // Refresh worktree list in UI + setWorktreeRefreshKey((k) => k + 1); + } else if (!result.success) { + logger.error( + `Failed to create worktree for branch "${finalBranchName}":`, + result.error + ); + toast.error('Failed to create worktree', { + description: result.error || 'An error occurred', + }); + return; // Don't proceed with update if worktree creation failed + } + } + } catch (error) { + logger.error('Error creating worktree:', error); + toast.error('Failed to create worktree', { + description: error instanceof Error ? error.message : 'An error occurred', + }); + return; // Don't proceed with update if worktree creation failed + } + } + + // Use the final branch name in updates + const finalUpdates = { + ...updates, + branchName: finalBranchName, + }; + const api = getHttpApiClient(); const featureIds = Array.from(selectedFeatureIds); - const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates); + const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates); if (result.success) { // Update local state featureIds.forEach((featureId) => { - updateFeature(featureId, updates); + updateFeature(featureId, finalUpdates); }); toast.success(`Updated ${result.updatedCount} features`); exitSelectionMode(); @@ -517,7 +603,17 @@ export function BoardView() { toast.error('Failed to update features'); } }, - [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode] + [ + currentProject, + selectedFeatureIds, + updateFeature, + exitSelectionMode, + currentWorktreeBranch, + getWorktrees, + setWorktrees, + setCurrentWorktree, + setWorktreeRefreshKey, + ] ); // Handler for bulk deleting multiple features @@ -1325,6 +1421,9 @@ export function BoardView() { onClose={() => setShowMassEditDialog(false)} selectedFeatures={selectedFeatures} onApply={handleBulkUpdate} + branchSuggestions={branchSuggestions} + branchCardCounts={branchCardCounts} + currentBranch={currentWorktreeBranch || undefined} /> {/* Board Background Modal */} diff --git a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx index 30042a4c..2be7d32f 100644 --- a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx @@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label'; import { AlertCircle } from 'lucide-react'; import { modelSupportsThinking } from '@/lib/utils'; import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store'; -import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared'; +import { TestingTabContent, PrioritySelect, PlanningModeSelect, WorkModeSelector } from '../shared'; +import type { WorkMode } from '../shared'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types'; import { cn } from '@/lib/utils'; @@ -23,7 +24,10 @@ interface MassEditDialogProps { open: boolean; onClose: () => void; selectedFeatures: Feature[]; - onApply: (updates: Partial) => Promise; + onApply: (updates: Partial, workMode: WorkMode) => Promise; + branchSuggestions: string[]; + branchCardCounts?: Record; + currentBranch?: string; } interface ApplyState { @@ -33,6 +37,7 @@ interface ApplyState { requirePlanApproval: boolean; priority: boolean; skipTests: boolean; + branchName: boolean; } function getMixedValues(features: Feature[]): Record { @@ -47,6 +52,7 @@ function getMixedValues(features: Feature[]): Record { ), priority: !features.every((f) => f.priority === first.priority), skipTests: !features.every((f) => f.skipTests === first.skipTests), + branchName: !features.every((f) => f.branchName === first.branchName), }; } @@ -97,7 +103,15 @@ function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: Fi ); } -export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: MassEditDialogProps) { +export function MassEditDialog({ + open, + onClose, + selectedFeatures, + onApply, + branchSuggestions, + branchCardCounts, + currentBranch, +}: MassEditDialogProps) { const [isApplying, setIsApplying] = useState(false); // Track which fields to apply @@ -108,6 +122,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas requirePlanApproval: false, priority: false, skipTests: false, + branchName: false, }); // Field values @@ -118,6 +133,18 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas const [priority, setPriority] = useState(2); const [skipTests, setSkipTests] = useState(false); + // Work mode and branch name state + const [workMode, setWorkMode] = useState(() => { + // Derive initial work mode from first selected feature's branchName + if (selectedFeatures.length > 0 && selectedFeatures[0].branchName) { + return 'custom'; + } + return 'current'; + }); + const [branchName, setBranchName] = useState(() => { + return getInitialValue(selectedFeatures, 'branchName', '') as string; + }); + // Calculate mixed values const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]); @@ -131,6 +158,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas requirePlanApproval: false, priority: false, skipTests: false, + branchName: false, }); setModel(getInitialValue(selectedFeatures, 'model', 'sonnet') as ModelAlias); setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel); @@ -138,6 +166,10 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false)); setPriority(getInitialValue(selectedFeatures, 'priority', 2)); setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false)); + // Reset work mode and branch name + const initialBranchName = getInitialValue(selectedFeatures, 'branchName', '') as string; + setBranchName(initialBranchName); + setWorkMode(initialBranchName ? 'custom' : 'current'); } }, [open, selectedFeatures]); @@ -150,6 +182,12 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval; if (applyState.priority) updates.priority = priority; if (applyState.skipTests) updates.skipTests = skipTests; + if (applyState.branchName) { + // For 'current' mode, use empty string (work on current branch) + // For 'auto' mode, use empty string (will be auto-generated) + // For 'custom' mode, use the specified branch name + updates.branchName = workMode === 'custom' ? branchName : ''; + } if (Object.keys(updates).length === 0) { onClose(); @@ -158,7 +196,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas setIsApplying(true); try { - await onApply(updates); + await onApply(updates, workMode); onClose(); } finally { setIsApplying(false); @@ -293,6 +331,25 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas testIdPrefix="mass-edit" /> + + {/* Branch / Work Mode */} + setApplyState((prev) => ({ ...prev, branchName: apply }))} + > + +
From d4076ad0cef4932d42c2cb02cdc3d20f3dd31c46 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:37:26 +0100 Subject: [PATCH 29/40] refactor: address CodeRabbit PR feedback Improvements based on CodeRabbit review comments: 1. Use getPrimaryWorktreeBranch for consistent branch detection - Replace hardcoded 'main' fallback with getPrimaryWorktreeBranch() - Ensures auto-generated branch names respect the repo's actual primary branch - Handles repos using 'master' or other primary branch names 2. Extract worktree auto-selection logic to helper function - Create addAndSelectWorktree helper to eliminate code duplication - Use helper in both onWorktreeAutoSelect and handleBulkUpdate - Reduces maintenance burden and ensures consistent behavior These changes improve code consistency and maintainability without affecting functionality. Co-Authored-By: Claude Sonnet 4.5 --- apps/ui/src/components/views/board-view.tsx | 77 +++++++++------------ 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 58136ebc..8f43b677 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -422,6 +422,31 @@ export function BoardView() { const selectedWorktreeBranch = currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main'; + // Helper function to add and select a worktree + const addAndSelectWorktree = useCallback( + (worktreeResult: { path: string; branch: string }) => { + if (!currentProject) return; + + const currentWorktrees = getWorktrees(currentProject.path); + const existingWorktree = currentWorktrees.find((w) => w.branch === worktreeResult.branch); + + // Only add if it doesn't already exist (to avoid duplicates) + if (!existingWorktree) { + const newWorktreeInfo = { + path: worktreeResult.path, + branch: worktreeResult.branch, + isMain: false, + isCurrent: false, + hasWorktree: true, + }; + setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); + } + // Select the worktree (whether it existed or was just added) + setCurrentWorktree(currentProject.path, worktreeResult.path, worktreeResult.branch); + }, + [currentProject, getWorktrees, setWorktrees, setCurrentWorktree] + ); + // Extract all action handlers into a hook const { handleAddFeature, @@ -467,26 +492,7 @@ export function BoardView() { outputFeature, projectPath: currentProject?.path || null, onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1), - onWorktreeAutoSelect: (newWorktree) => { - if (!currentProject) return; - // Check if worktree already exists in the store (by branch name) - const currentWorktrees = getWorktrees(currentProject.path); - const existingWorktree = currentWorktrees.find((w) => w.branch === newWorktree.branch); - - // Only add if it doesn't already exist (to avoid duplicates) - if (!existingWorktree) { - const newWorktreeInfo = { - path: newWorktree.path, - branch: newWorktree.branch, - isMain: false, - isCurrent: false, - hasWorktree: true, - }; - setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); - } - // Select the worktree (whether it existed or was just added) - setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch); - }, + onWorktreeAutoSelect: addAndSelectWorktree, currentWorktreeBranch, }); @@ -507,7 +513,8 @@ export function BoardView() { finalBranchName = undefined; } else if (workMode === 'auto') { // Auto-generate a branch name based on current branch and timestamp - const baseBranch = currentWorktreeBranch || 'main'; + const baseBranch = + currentWorktreeBranch || getPrimaryWorktreeBranch(currentProject.path) || 'main'; const timestamp = Date.now(); const randomSuffix = Math.random().toString(36).substring(2, 6); finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`; @@ -532,28 +539,7 @@ export function BoardView() { }` ); // Auto-select the worktree when creating/using it for bulk update - const currentWorktrees = getWorktrees(currentProject.path); - const existingWorktree = currentWorktrees.find( - (w) => w.branch === result.worktree.branch - ); - - // Only add if it doesn't already exist (to avoid duplicates) - if (!existingWorktree) { - const newWorktreeInfo = { - path: result.worktree.path, - branch: result.worktree.branch, - isMain: false, - isCurrent: false, - hasWorktree: true, - }; - setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); - } - // Select the worktree (whether it existed or was just added) - setCurrentWorktree( - currentProject.path, - result.worktree.path, - result.worktree.branch - ); + addAndSelectWorktree(result.worktree); // Refresh worktree list in UI setWorktreeRefreshKey((k) => k + 1); } else if (!result.success) { @@ -609,9 +595,8 @@ export function BoardView() { updateFeature, exitSelectionMode, currentWorktreeBranch, - getWorktrees, - setWorktrees, - setCurrentWorktree, + getPrimaryWorktreeBranch, + addAndSelectWorktree, setWorktreeRefreshKey, ] ); From cc4f39a6abb80dc660281dda3d53dd6bf94fc7f0 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:38:09 +0100 Subject: [PATCH 30/40] chore: fix formatting issues for CI Fix Prettier formatting in two files: - apps/server/src/lib/sdk-options.ts: Split long arrays to one item per line - docs/docker-isolation.md: Align markdown table columns Resolves CI format check failures. Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/lib/sdk-options.ts | 24 ++++++++++++++++++++++-- docs/docker-isolation.md | 15 +++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index ff3d6067..cc1df2f5 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -129,10 +129,30 @@ export const TOOL_PRESETS = { specGeneration: ['Read', 'Glob', 'Grep'] as const, /** Full tool access for feature implementation */ - fullAccess: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + fullAccess: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, /** Tools for chat/interactive mode */ - chat: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + chat: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, } as const; /** diff --git a/docs/docker-isolation.md b/docs/docker-isolation.md index ad7c712a..379e8c0d 100644 --- a/docs/docker-isolation.md +++ b/docs/docker-isolation.md @@ -136,12 +136,11 @@ volumes: ## Troubleshooting -| Problem | Solution | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | -| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | -| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | -| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. | -| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. | +| Problem | Solution | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | +| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | +| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | +| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. | +| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. | | File permission errors | Rebuild with `UID=$(id -u) GID=$(id -g) docker-compose build` to match container user to your host user. See [Fixing File Permission Issues](#fixing-file-permission-issues). | - From 7ef525effac6b6058b3555103dde9dfe09ec71f9 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:51:20 +0100 Subject: [PATCH 31/40] fix: clarify comments on branch name handling in BoardView Updated comments in BoardView to better explain the behavior of the 'current' work mode. The changes specify that an empty string clears the branch assignment, allowing work to proceed on the main/current branch. This enhances code readability and understanding of branch management logic. --- apps/ui/src/components/views/board-view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 8f43b677..f51357d0 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -503,14 +503,14 @@ export function BoardView() { try { // Determine final branch name based on work mode: - // - 'current': No branch name, work on current branch (no worktree) + // - 'current': Empty string to clear branch assignment (work on main/current branch) // - 'auto': Auto-generate branch name based on current branch // - 'custom': Use the provided branch name let finalBranchName: string | undefined; if (workMode === 'current') { - // No worktree isolation - work directly on current branch - finalBranchName = undefined; + // Empty string clears the branch assignment, moving features to main/current branch + finalBranchName = ''; } else if (workMode === 'auto') { // Auto-generate a branch name based on current branch and timestamp const baseBranch = From 62af2031f68914a3b2982c3e2c175de6f369f668 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 19:33:09 +0100 Subject: [PATCH 32/40] feat: enhance dev server URL handling and improve accessibility - Added URL and URLSearchParams as readonly globals in ESLint configuration. - Updated WorktreeActionsDropdown and WorktreeTab components to include aria-labels for better accessibility. - Implemented error handling for dev server URL opening, ensuring only valid HTTP/HTTPS protocols are used and providing user feedback for errors. These changes improve user experience and accessibility when interacting with the dev server functionality. --- apps/ui/eslint.config.mjs | 2 + .../components/worktree-actions-dropdown.tsx | 8 +++- .../components/worktree-tab.tsx | 37 ++++++++++++------- .../worktree-panel/hooks/use-dev-servers.ts | 33 ++++++++++++++++- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/apps/ui/eslint.config.mjs b/apps/ui/eslint.config.mjs index d7bc54d4..6db837e3 100644 --- a/apps/ui/eslint.config.mjs +++ b/apps/ui/eslint.config.mjs @@ -70,6 +70,8 @@ const eslintConfig = defineConfig([ AbortSignal: 'readonly', Audio: 'readonly', ScrollBehavior: 'readonly', + URL: 'readonly', + URLSearchParams: 'readonly', // Timers setTimeout: 'readonly', setInterval: 'readonly', diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx index c7d8f26b..27cc38d8 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx @@ -143,8 +143,12 @@ export function WorktreeActionsDropdown({ Dev Server Running (:{devServerInfo?.port}) - onOpenDevServerUrl(worktree)} className="text-xs"> - + onOpenDevServerUrl(worktree)} + className="text-xs" + aria-label={`Open dev server on port ${devServerInfo?.port} in browser`} + > + onOpenDevServerUrl(worktree)} - title={`Open dev server (port ${devServerInfo?.port})`} - > - - + + + + + + +

Open dev server (:{devServerInfo?.port})

+
+
+
)} { const serverInfo = runningDevServers.get(getWorktreeKey(worktree)); - if (serverInfo) { - window.open(serverInfo.url, '_blank'); + if (!serverInfo) { + logger.warn('No dev server info found for worktree:', getWorktreeKey(worktree)); + toast.error('Dev server not found', { + description: 'The dev server may have stopped. Try starting it again.', + }); + return; + } + + try { + // Rewrite URL hostname to match the current browser's hostname. + // This ensures dev server URLs work when accessing Automaker from + // remote machines (e.g., 192.168.x.x or hostname.local instead of localhost). + const devServerUrl = new URL(serverInfo.url); + + // Security: Only allow http/https protocols to prevent potential attacks + // via data:, javascript:, file:, or other dangerous URL schemes + if (devServerUrl.protocol !== 'http:' && devServerUrl.protocol !== 'https:') { + logger.error('Invalid dev server URL protocol:', devServerUrl.protocol); + toast.error('Invalid dev server URL', { + description: 'The server returned an unsupported URL protocol.', + }); + return; + } + + devServerUrl.hostname = window.location.hostname; + window.open(devServerUrl.toString(), '_blank'); + } catch (error) { + logger.error('Failed to parse dev server URL:', error); + toast.error('Failed to open dev server', { + description: 'The server URL could not be processed. Please try again.', + }); } }, [runningDevServers, getWorktreeKey] From f4390bc82f598be12277f570a4e0c64d96d341b4 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 19:43:20 +0100 Subject: [PATCH 33/40] security: add noopener,noreferrer to window.open calls Add 'noopener,noreferrer' parameter to all window.open() calls with target='_blank' to prevent tabnabbing attacks. This prevents the newly opened page from accessing window.opener, protecting against potential security vulnerabilities. Affected files: - use-dev-servers.ts: Dev server URL links - worktree-actions-dropdown.tsx: PR URL links - create-pr-dialog.tsx: PR creation and browser fallback links Co-Authored-By: Claude Sonnet 4.5 --- .../views/board-view/dialogs/create-pr-dialog.tsx | 11 +++++++---- .../components/worktree-actions-dropdown.tsx | 2 +- .../worktree-panel/hooks/use-dev-servers.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx index a8ba8ee5..3abbb75f 100644 --- a/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx @@ -117,7 +117,7 @@ export function CreatePRDialog({ description: `PR already exists for ${result.result.branch}`, action: { label: 'View PR', - onClick: () => window.open(result.result!.prUrl!, '_blank'), + onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'), }, }); } else { @@ -125,7 +125,7 @@ export function CreatePRDialog({ description: `PR created from ${result.result.branch}`, action: { label: 'View PR', - onClick: () => window.open(result.result!.prUrl!, '_blank'), + onClick: () => window.open(result.result!.prUrl!, '_blank', 'noopener,noreferrer'), }, }); } @@ -251,7 +251,10 @@ export function CreatePRDialog({

Your PR is ready for review

- @@ -277,7 +280,7 @@ export function CreatePRDialog({