mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +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 { getNotificationService } from './notification-service.js';
|
||||||
import { extractSummary } from './spec-parser.js';
|
import { extractSummary } from './spec-parser.js';
|
||||||
import { AgentExecutor } from './agent-executor.js';
|
import { AgentExecutor } from './agent-executor.js';
|
||||||
import {
|
import { PipelineOrchestrator } from './pipeline-orchestrator.js';
|
||||||
PipelineOrchestrator,
|
|
||||||
type PipelineStatusInfo as OrchestratorPipelineStatusInfo,
|
|
||||||
} from './pipeline-orchestrator.js';
|
|
||||||
import { TestRunnerService } from './test-runner-service.js';
|
import { TestRunnerService } from './test-runner-service.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
// ParsedTask and PlanSpec types are imported from @automaker/types
|
// 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
|
// Spec parsing functions are imported from spec-parser.js
|
||||||
|
|
||||||
// Feature type is imported from feature-loader.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
|
* 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:
|
* 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
|
* - 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)
|
* - 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 projectPath - Path to the project
|
||||||
* @param featureId - ID of the feature to resume
|
* @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
|
* 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.
|
* resumed later using the resume functionality.
|
||||||
*
|
*
|
||||||
* Note: Features with pipeline_* statuses are preserved rather than overwritten
|
* 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.
|
* the correct pipeline step after a restart.
|
||||||
*
|
*
|
||||||
* @param projectPath - Path to the project
|
* @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.
|
* Build a focused prompt for executing a single task.
|
||||||
* Each task gets minimal context to keep the agent focused.
|
* Each task gets minimal context to keep the agent focused.
|
||||||
|
|||||||
Reference in New Issue
Block a user