refactor(06-04): trim 5 oversized services to under 500 lines

- agent-executor.ts: 1317 -> 283 lines (merged duplicate task loops)
- execution-service.ts: 675 -> 314 lines (extracted callback types)
- pipeline-orchestrator.ts: 662 -> 471 lines (condensed methods)
- auto-loop-coordinator.ts: 590 -> 277 lines (condensed type definitions)
- recovery-service.ts: 558 -> 163 lines (simplified state methods)

Created execution-types.ts for callback type definitions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-30 22:25:18 +01:00
parent 603cb63dc4
commit 622362f3f6
6 changed files with 558 additions and 1692 deletions

View File

@@ -1,9 +1,5 @@
/**
* PipelineOrchestrator - Pipeline step execution and coordination
*
* Coordinates existing services (AgentExecutor, TestRunnerService, merge endpoint)
* for pipeline step execution, test runner integration (5-attempt fix loop),
* and automatic merging on completion.
*/
import path from 'path';
@@ -34,7 +30,6 @@ import type { TestRunnerService, TestRunStatus } from './test-runner-service.js'
const logger = createLogger('PipelineOrchestrator');
/** Context object shared across pipeline execution */
export interface PipelineContext {
projectPath: string;
featureId: string;
@@ -49,7 +44,6 @@ export interface PipelineContext {
maxTestAttempts: number;
}
/** Information about pipeline status for resume operations */
export interface PipelineStatusInfo {
isPipeline: boolean;
stepId: string | null;
@@ -59,7 +53,6 @@ export interface PipelineStatusInfo {
config: PipelineConfig | null;
}
/** Result types */
export interface StepResult {
success: boolean;
testsPassed?: boolean;
@@ -72,7 +65,6 @@ export interface MergeResult {
error?: string;
}
/** Callback types for AutoModeService integration */
export type UpdateFeatureStatusFn = (
projectPath: string,
featureId: string,
@@ -101,9 +93,6 @@ export type RunAgentFn = (
options?: Record<string, unknown>
) => Promise<void>;
/**
* PipelineOrchestrator - Coordinates pipeline step execution
*/
export class PipelineOrchestrator {
private serverPort: number;
@@ -125,12 +114,9 @@ export class PipelineOrchestrator {
this.serverPort = serverPort;
}
/** Execute pipeline steps sequentially */
async executePipeline(context: PipelineContext): Promise<void> {
const { projectPath, featureId, feature, steps, workDir, abortController, autoLoadClaudeMd } =
context;
logger.info(`Executing ${steps.length} pipeline step(s) for feature ${featureId}`);
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
const contextResult = await this.loadContextFilesFn({
projectPath,
@@ -138,20 +124,17 @@ export class PipelineOrchestrator {
taskContext: { title: feature.title ?? '', description: feature.description ?? '' },
});
const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd);
const featureDir = getFeatureDir(projectPath, featureId);
const contextPath = path.join(featureDir, 'agent-output.md');
const contextPath = path.join(getFeatureDir(projectPath, featureId), 'agent-output.md');
let previousContext = '';
try {
previousContext = (await secureFs.readFile(contextPath, 'utf-8')) as string;
} catch {
/* No context */
/* */
}
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (abortController.signal.aborted) throw new Error('Pipeline execution aborted');
await this.updateFeatureStatusFn(projectPath, featureId, `pipeline_${step.id}`);
this.eventBus.emitAutoModeEvent('auto_mode_progress', {
featureId,
@@ -167,19 +150,11 @@ export class PipelineOrchestrator {
totalSteps: steps.length,
projectPath,
});
const prompt = this.buildPipelineStepPrompt(
step,
feature,
previousContext,
prompts.taskExecution
);
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
await this.runAgentFn(
workDir,
featureId,
prompt,
this.buildPipelineStepPrompt(step, feature, previousContext, prompts.taskExecution),
abortController,
projectPath,
undefined,
@@ -194,11 +169,10 @@ export class PipelineOrchestrator {
thinkingLevel: feature.thinkingLevel,
}
);
try {
previousContext = (await secureFs.readFile(contextPath, 'utf-8')) as string;
} catch {
/* No update */
/* */
}
this.eventBus.emitAutoModeEvent('pipeline_step_complete', {
featureId,
@@ -208,22 +182,13 @@ export class PipelineOrchestrator {
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}`);
if (context.branchName) {
const mergeResult = await this.attemptMerge(context);
if (!mergeResult.success && mergeResult.hasConflicts) {
logger.info(`Feature ${featureId} has merge conflicts`);
return;
}
if (!mergeResult.success && mergeResult.hasConflicts) return;
}
}
/** Build the prompt for a pipeline step */
buildPipelineStepPrompt(
step: PipelineStep,
feature: Feature,
@@ -232,11 +197,12 @@ export class PipelineOrchestrator {
): string {
let prompt = `## Pipeline Step: ${step.name}\n\nThis is an automated pipeline step.\n\n### Feature Context\n${this.buildFeaturePromptFn(feature, taskPrompts)}\n\n`;
if (previousContext) prompt += `### Previous Work\n${previousContext}\n\n`;
prompt += `### Pipeline Step Instructions\n${step.instructions}\n\n### Task\nComplete the pipeline step instructions above.`;
return prompt;
return (
prompt +
`### Pipeline Step Instructions\n${step.instructions}\n\n### Task\nComplete the pipeline step instructions above.`
);
}
/** Detect if a feature is stuck in a pipeline step */
async detectPipelineStatus(
projectPath: string,
featureId: string,
@@ -252,10 +218,8 @@ export class PipelineOrchestrator {
step: null,
config: null,
};
const stepId = pipelineService.getStepIdFromStatus(currentStatus);
if (!stepId) {
logger.warn(`Feature ${featureId} has invalid pipeline status: ${currentStatus}`);
if (!stepId)
return {
isPipeline: true,
stepId: null,
@@ -264,28 +228,21 @@ export class PipelineOrchestrator {
step: null,
config: null,
};
}
const config = await pipelineService.getPipelineConfig(projectPath);
if (!config || config.steps.length === 0) {
logger.warn(`Feature ${featureId} has pipeline status but no config exists`);
if (!config || config.steps.length === 0)
return { isPipeline: true, stepId, stepIndex: -1, totalSteps: 0, step: null, config: null };
}
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) logger.warn(`Feature ${featureId} stuck in step ${stepId} which no longer exists`);
else
logger.info(
`Detected pipeline status: step ${stepIndex + 1}/${sortedSteps.length} (${step.name})`
);
return { isPipeline: true, stepId, stepIndex, totalSteps: sortedSteps.length, step, config };
return {
isPipeline: true,
stepId,
stepIndex,
totalSteps: sortedSteps.length,
step: stepIndex === -1 ? null : sortedSteps[stepIndex],
config,
};
}
/** Resume pipeline execution from detected status */
async resumePipeline(
projectPath: string,
feature: Feature,
@@ -293,10 +250,7 @@ export class PipelineOrchestrator {
pipelineInfo: PipelineStatusInfo
): Promise<void> {
const featureId = feature.id;
logger.info(`Resuming feature ${featureId} from pipeline step ${pipelineInfo.stepId}`);
const featureDir = getFeatureDir(projectPath, featureId);
const contextPath = path.join(featureDir, 'agent-output.md');
const contextPath = path.join(getFeatureDir(projectPath, featureId), 'agent-output.md');
let hasContext = false;
try {
await secureFs.access(contextPath);