From 84461d65540ed8feb446012dc2480ed91adf8c97 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 27 Jan 2026 18:01:40 +0100 Subject: [PATCH] refactor(04-02): remove duplicated pipeline methods from AutoModeService - Delete executePipelineSteps method (~115 lines) - Delete buildPipelineStepPrompt method (~38 lines) - Delete resumePipelineFeature method (~88 lines) - Delete resumeFromPipelineStep method (~195 lines) - Delete detectPipelineStatus method (~104 lines) - Remove unused PipelineStatusInfo interface (~18 lines) - Update comments to reference PipelineOrchestrator Total reduction: ~546 lines (4150 -> 3604 lines) --- apps/server/src/services/auto-mode-service.ts | 597 +----------------- 1 file changed, 3 insertions(+), 594 deletions(-) diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 0a6d37e9..5ce3b78f 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -85,36 +85,13 @@ import { import { getNotificationService } from './notification-service.js'; import { extractSummary } from './spec-parser.js'; import { AgentExecutor } from './agent-executor.js'; -import { - PipelineOrchestrator, - type PipelineStatusInfo as OrchestratorPipelineStatusInfo, -} from './pipeline-orchestrator.js'; +import { PipelineOrchestrator } from './pipeline-orchestrator.js'; import { TestRunnerService } from './test-runner-service.js'; const execAsync = promisify(exec); // ParsedTask and PlanSpec types are imported from @automaker/types -/** - * Information about pipeline status when resuming a feature. - * Used to determine how to handle features stuck in pipeline execution. - * - * @property {boolean} isPipeline - Whether the feature is in a pipeline step - * @property {string | null} stepId - ID of the current pipeline step (e.g., 'step_123') - * @property {number} stepIndex - Index of the step in the sorted pipeline steps (-1 if not found) - * @property {number} totalSteps - Total number of steps in the pipeline - * @property {PipelineStep | null} step - The pipeline step configuration, or null if step not found - * @property {PipelineConfig | null} config - The full pipeline configuration, or null if no pipeline - */ -interface PipelineStatusInfo { - isPipeline: boolean; - stepId: string | null; - stepIndex: number; - totalSteps: number; - step: PipelineStep | null; - config: PipelineConfig | null; -} - // Spec parsing functions are imported from spec-parser.js // Feature type is imported from feature-loader.js @@ -1387,161 +1364,6 @@ export class AutoModeService { } } - /** - * Execute pipeline steps sequentially after initial feature implementation - */ - private async executePipelineSteps( - projectPath: string, - featureId: string, - feature: Feature, - steps: PipelineStep[], - workDir: string, - abortController: AbortController, - autoLoadClaudeMd: boolean - ): Promise { - logger.info(`Executing ${steps.length} pipeline step(s) for feature ${featureId}`); - - // Get customized prompts from settings - const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]'); - - // Load context files once with feature context for smart memory selection - const contextResult = await loadContextFiles({ - projectPath, - fsModule: secureFs as Parameters[0]['fsModule'], - taskContext: { - title: feature.title ?? '', - description: feature.description ?? '', - }, - }); - const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); - - // Load previous agent output for context continuity - const featureDir = getFeatureDir(projectPath, featureId); - const contextPath = path.join(featureDir, 'agent-output.md'); - let previousContext = ''; - try { - previousContext = (await secureFs.readFile(contextPath, 'utf-8')) as string; - } catch { - // No previous context - } - - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const pipelineStatus = `pipeline_${step.id}`; - - // Update feature status to current pipeline step - await this.updateFeatureStatus(projectPath, featureId, pipelineStatus); - - this.eventBus.emitAutoModeEvent('auto_mode_progress', { - featureId, - branchName: feature.branchName ?? null, - content: `Starting pipeline step ${i + 1}/${steps.length}: ${step.name}`, - projectPath, - }); - - this.eventBus.emitAutoModeEvent('pipeline_step_started', { - featureId, - stepId: step.id, - stepName: step.name, - stepIndex: i, - totalSteps: steps.length, - projectPath, - }); - - // Build prompt for this pipeline step - const prompt = this.buildPipelineStepPrompt( - step, - feature, - previousContext, - prompts.taskExecution - ); - - // Get model from feature - const model = resolveModelString(feature.model, DEFAULT_MODELS.claude); - - // Run the agent for this pipeline step - await this.runAgent( - workDir, - featureId, - prompt, - abortController, - projectPath, - undefined, // no images for pipeline steps - model, - { - projectPath, - planningMode: 'skip', // Pipeline steps don't need planning - requirePlanApproval: false, - previousContent: previousContext, - systemPrompt: contextFilesPrompt || undefined, - autoLoadClaudeMd, - thinkingLevel: feature.thinkingLevel, - } - ); - - // Load updated context for next step - try { - previousContext = (await secureFs.readFile(contextPath, 'utf-8')) as string; - } catch { - // No context update - } - - this.eventBus.emitAutoModeEvent('pipeline_step_complete', { - featureId, - stepId: step.id, - stepName: step.name, - stepIndex: i, - totalSteps: steps.length, - projectPath, - }); - - logger.info( - `Pipeline step ${i + 1}/${steps.length} (${step.name}) completed for feature ${featureId}` - ); - } - - logger.info(`All pipeline steps completed for feature ${featureId}`); - } - - /** - * Build the prompt for a pipeline step - */ - private buildPipelineStepPrompt( - step: PipelineStep, - feature: Feature, - previousContext: string, - taskExecutionPrompts: { - implementationInstructions: string; - playwrightVerificationInstructions: string; - } - ): string { - let prompt = `## Pipeline Step: ${step.name} - -This is an automated pipeline step following the initial feature implementation. - -### Feature Context -${this.buildFeaturePrompt(feature, taskExecutionPrompts)} - -`; - - if (previousContext) { - prompt += `### Previous Work -The following is the output from the previous work on this feature: - -${previousContext} - -`; - } - - prompt += `### Pipeline Step Instructions -${step.instructions} - -### Task -Complete the pipeline step instructions above. Review the previous work and apply the required changes or actions.`; - - return prompt; - } - /** * Stop a specific feature */ @@ -1569,7 +1391,7 @@ Complete the pipeline step instructions above. Review the previous work and appl * This method handles interrupted features regardless of whether they have saved context: * - With context: Continues from where the agent left off using the saved agent-output.md * - Without context: Starts fresh execution (feature was interrupted before any agent output) - * - Pipeline features: Delegates to resumePipelineFeature for specialized handling + * - Pipeline features: Delegates to PipelineOrchestrator for specialized handling * * @param projectPath - Path to the project * @param featureId - ID of the feature to resume @@ -1686,314 +1508,6 @@ Complete the pipeline step instructions above. Review the previous work and appl } } - /** - * Resume a feature that crashed during pipeline execution. - * Handles multiple edge cases to ensure robust recovery: - * - No context file: Restart entire pipeline from beginning - * - Step deleted from config: Complete feature without remaining pipeline steps - * - Valid step exists: Resume from the crashed step and continue - * - * @param {string} projectPath - Absolute path to the project directory - * @param {Feature} feature - The feature object (already loaded to avoid redundant reads) - * @param {boolean} useWorktrees - Whether to use git worktrees for isolation - * @param {PipelineStatusInfo} pipelineInfo - Information about the pipeline status from detectPipelineStatus() - * @returns {Promise} Resolves when resume operation completes or throws on error - * @throws {Error} If pipeline config is null but stepIndex is valid (should never happen) - * @private - */ - private async resumePipelineFeature( - projectPath: string, - feature: Feature, - useWorktrees: boolean, - pipelineInfo: PipelineStatusInfo - ): Promise { - const featureId = feature.id; - 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'); - - return this.executeFeature(projectPath, featureId, useWorktrees, false, undefined, { - _calledInternally: true, - }); - } - - // 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 finalStatus = feature.skipTests ? 'waiting_approval' : 'verified'; - - await this.updateFeatureStatus(projectPath, featureId, finalStatus); - - this.eventBus.emitAutoModeEvent('auto_mode_feature_complete', { - featureId, - featureName: feature.title, - branchName: feature.branchName ?? null, - passes: true, - message: - 'Pipeline step no longer exists - feature completed without remaining pipeline steps', - projectPath, - }); - - 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, - feature, - useWorktrees, - pipelineInfo.stepIndex, - pipelineInfo.config - ); - } - - /** - * Resume pipeline execution from a specific step index. - * Re-executes the step that crashed (to handle partial completion), - * then continues executing all remaining pipeline steps in order. - * - * This method handles the complete pipeline resume workflow: - * - Validates feature and step index - * - Locates or creates git worktree if needed - * - Executes remaining steps starting from the crashed step - * - Updates feature status to verified/waiting_approval when complete - * - Emits progress events throughout execution - * - * @param {string} projectPath - Absolute path to the project directory - * @param {Feature} feature - The feature object (already loaded to avoid redundant reads) - * @param {boolean} useWorktrees - Whether to use git worktrees for isolation - * @param {number} startFromStepIndex - Zero-based index of the step to resume from - * @param {PipelineConfig} pipelineConfig - Pipeline config passed from detectPipelineStatus to avoid re-reading - * @returns {Promise} Resolves when pipeline execution completes successfully - * @throws {Error} If feature not found, step index invalid, or pipeline execution fails - * @private - */ - private async resumeFromPipelineStep( - projectPath: string, - feature: Feature, - useWorktrees: boolean, - startFromStepIndex: number, - pipelineConfig: PipelineConfig - ): Promise { - const featureId = feature.id; - - // Sort all steps first - const allSortedSteps = [...pipelineConfig.steps].sort((a, b) => a.order - b.order); - - // Get the current step we're resuming from (using the index from unfiltered list) - if (startFromStepIndex < 0 || startFromStepIndex >= allSortedSteps.length) { - throw new Error(`Invalid step index: ${startFromStepIndex}`); - } - const currentStep = allSortedSteps[startFromStepIndex]; - - // Filter out excluded pipeline steps - const excludedStepIds = new Set(feature.excludedPipelineSteps || []); - - // Check if the current step is excluded - // If so, use getNextStatus to find the appropriate next step - if (excludedStepIds.has(currentStep.id)) { - logger.info( - `Current step ${currentStep.id} is excluded for feature ${featureId}, finding next valid step` - ); - const nextStatus = pipelineService.getNextStatus( - `pipeline_${currentStep.id}`, - pipelineConfig, - feature.skipTests ?? false, - feature.excludedPipelineSteps - ); - - // If next status is not a pipeline step, feature is done - if (!pipelineService.isPipelineStatus(nextStatus)) { - await this.updateFeatureStatus(projectPath, featureId, nextStatus); - this.eventBus.emitAutoModeEvent('auto_mode_feature_complete', { - featureId, - featureName: feature.title, - branchName: feature.branchName ?? null, - passes: true, - message: 'Pipeline completed (remaining steps excluded)', - projectPath, - }); - return; - } - - // Find the next step and update the start index - const nextStepId = pipelineService.getStepIdFromStatus(nextStatus); - const nextStepIndex = allSortedSteps.findIndex((s) => s.id === nextStepId); - if (nextStepIndex === -1) { - throw new Error(`Next step ${nextStepId} not found in pipeline config`); - } - startFromStepIndex = nextStepIndex; - } - - // Get steps to execute (from startFromStepIndex onwards, excluding excluded steps) - const stepsToExecute = allSortedSteps - .slice(startFromStepIndex) - .filter((step) => !excludedStepIds.has(step.id)); - - // If no steps left to execute, complete the feature - if (stepsToExecute.length === 0) { - const finalStatus = feature.skipTests ? 'waiting_approval' : 'verified'; - await this.updateFeatureStatus(projectPath, featureId, finalStatus); - this.eventBus.emitAutoModeEvent('auto_mode_feature_complete', { - featureId, - featureName: feature.title, - branchName: feature.branchName ?? null, - passes: true, - message: 'Pipeline completed (all remaining steps excluded)', - projectPath, - }); - return; - } - - // Use the filtered steps for counting - const sortedSteps = allSortedSteps.filter((step) => !excludedStepIds.has(step.id)); - - logger.info( - `Resuming pipeline for feature ${featureId} from step ${startFromStepIndex + 1}/${sortedSteps.length}` - ); - - const runningEntry = this.acquireRunningFeature({ - featureId, - projectPath, - isAutoMode: false, - allowReuse: true, - }); - const abortController = runningEntry.abortController; - runningEntry.branchName = feature.branchName ?? null; - - 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.worktreeResolver.findWorktreeForBranch(projectPath, branchName); - if (worktreePath) { - logger.info(`Using worktree for branch "${branchName}": ${worktreePath}`); - } else { - logger.warn(`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 - runningEntry.worktreePath = worktreePath; - runningEntry.branchName = branchName ?? null; - - // Emit resume event - this.eventBus.emitAutoModeEvent('auto_mode_feature_start', { - featureId, - projectPath, - branchName: branchName ?? null, - feature: { - id: featureId, - title: feature.title || 'Resuming Pipeline', - description: feature.description, - }, - }); - - this.eventBus.emitAutoModeEvent('auto_mode_progress', { - featureId, - projectPath, - branchName: branchName ?? null, - content: `Resuming from pipeline step ${startFromStepIndex + 1}/${sortedSteps.length}`, - }); - - // 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); - - logger.info(`Pipeline resume completed successfully for feature ${featureId}`); - - this.eventBus.emitAutoModeEvent('auto_mode_feature_complete', { - featureId, - featureName: feature.title, - branchName: feature.branchName ?? null, - passes: true, - message: 'Pipeline resumed and completed successfully', - projectPath, - }); - } catch (error) { - const errorInfo = classifyError(error); - - if (errorInfo.isAbort) { - this.eventBus.emitAutoModeEvent('auto_mode_feature_complete', { - featureId, - featureName: feature.title, - branchName: feature.branchName ?? null, - passes: false, - message: 'Pipeline resume stopped by user', - projectPath, - }); - } else { - logger.error(`Pipeline resume failed for feature ${featureId}:`, error); - await this.updateFeatureStatus(projectPath, featureId, 'backlog'); - this.eventBus.emitAutoModeEvent('auto_mode_error', { - featureId, - featureName: feature.title, - branchName: feature.branchName ?? null, - error: errorInfo.message, - errorType: errorInfo.type, - projectPath, - }); - } - } finally { - this.releaseRunningFeature(featureId); - } - } - /** * Follow up on a feature with additional instructions */ @@ -2762,7 +2276,7 @@ Format your response as a structured markdown document.`; * resumed later using the resume functionality. * * Note: Features with pipeline_* statuses are preserved rather than overwritten - * to 'interrupted'. This ensures that resumePipelineFeature() can pick up from + * to 'interrupted'. This ensures that pipeline resume can pick up from * the correct pipeline step after a restart. * * @param projectPath - Path to the project @@ -3543,111 +3057,6 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. }); } - /** - * Detect if a feature is stuck in a pipeline step and extract step information. - * Parses the feature status to determine if it's a pipeline status (e.g., 'pipeline_step_xyz'), - * loads the pipeline configuration, and validates that the step still exists. - * - * This method handles several scenarios: - * - Non-pipeline status: Returns default PipelineStatusInfo with isPipeline=false - * - Invalid pipeline status format: Returns isPipeline=true but null step info - * - Step deleted from config: Returns stepIndex=-1 to signal missing step - * - Valid pipeline step: Returns full step information and config - * - * @param {string} projectPath - Absolute path to the project directory - * @param {string} featureId - Unique identifier of the feature - * @param {FeatureStatusWithPipeline} currentStatus - Current feature status (may include pipeline step info) - * @returns {Promise} Information about the pipeline status and step - * @private - */ - private async detectPipelineStatus( - projectPath: string, - featureId: string, - currentStatus: FeatureStatusWithPipeline - ): Promise { - // 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 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 (!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.