From a9403651d464fed075f0f6dc6f47972f039e7f46 Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Fri, 2 Jan 2026 00:58:32 +0100 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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',