mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
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)
This commit is contained in:
@@ -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<void> {
|
||||
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<typeof loadContextFiles>[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<void>} 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<void> {
|
||||
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<void>} 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<void> {
|
||||
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<PipelineStatusInfo>} Information about the pipeline status and step
|
||||
* @private
|
||||
*/
|
||||
private async detectPipelineStatus(
|
||||
projectPath: string,
|
||||
featureId: string,
|
||||
currentStatus: FeatureStatusWithPipeline
|
||||
): Promise<PipelineStatusInfo> {
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user