feat(06-01): create AutoModeServiceFacade with all 23 methods

- Create facade.ts with per-project factory pattern
- Implement all 23 public methods from RESEARCH.md inventory:
  - Auto loop control: startAutoLoop, stopAutoLoop, isAutoLoopRunning, getAutoLoopConfig
  - Feature execution: executeFeature, stopFeature, resumeFeature, followUpFeature, verifyFeature, commitFeature
  - Status queries: getStatus, getStatusForProject, getActiveAutoLoopProjects, getActiveAutoLoopWorktrees, getRunningAgents, checkWorktreeCapacity, contextExists
  - Plan approval: resolvePlanApproval, waitForPlanApproval, hasPendingApproval, cancelPlanApproval
  - Analysis/recovery: analyzeProject, resumeInterruptedFeatures, detectOrphanedFeatures
  - Lifecycle: markAllRunningFeaturesInterrupted
- Use thin delegation pattern to underlying services
- Note: followUpFeature and analyzeProject require AutoModeService until full migration
This commit is contained in:
Shirone
2026-01-30 21:25:27 +01:00
parent da373ee3ea
commit 68ea80b6fe

View File

@@ -0,0 +1,913 @@
/**
* AutoModeServiceFacade - Clean interface for auto-mode functionality
*
* This facade provides a thin delegation layer over the extracted services,
* exposing all 23 public methods that routes currently call on AutoModeService.
*
* Key design decisions:
* - Per-project factory pattern (projectPath is implicit in method calls)
* - Clean method names (e.g., startAutoLoop instead of startAutoLoopForProject)
* - Thin delegation to underlying services - no new business logic
* - Maintains backward compatibility during transition period
*/
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import type { Feature } from '@automaker/types';
import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types';
import { createLogger, loadContextFiles, classifyError } from '@automaker/utils';
import { getFeatureDir } from '@automaker/platform';
import * as secureFs from '../../lib/secure-fs.js';
import { validateWorkingDirectory } from '../../lib/sdk-options.js';
import { getPromptCustomization } from '../../lib/settings-helpers.js';
import { TypedEventBus } from '../typed-event-bus.js';
import { ConcurrencyManager } from '../concurrency-manager.js';
import { WorktreeResolver } from '../worktree-resolver.js';
import { FeatureStateManager } from '../feature-state-manager.js';
import { PlanApprovalService } from '../plan-approval-service.js';
import { AutoLoopCoordinator, type AutoModeConfig } from '../auto-loop-coordinator.js';
import { ExecutionService } from '../execution-service.js';
import { RecoveryService } from '../recovery-service.js';
import { PipelineOrchestrator } from '../pipeline-orchestrator.js';
import { AgentExecutor } from '../agent-executor.js';
import { TestRunnerService } from '../test-runner-service.js';
import { FeatureLoader } from '../feature-loader.js';
import type { SettingsService } from '../settings-service.js';
import type { EventEmitter } from '../../lib/events.js';
import type {
FacadeOptions,
AutoModeStatus,
ProjectAutoModeStatus,
WorktreeCapacityInfo,
RunningAgentInfo,
OrphanedFeatureInfo,
} from './types.js';
const execAsync = promisify(exec);
const logger = createLogger('AutoModeServiceFacade');
/**
* Generate a unique key for worktree-scoped auto loop state
* (mirrors the function in AutoModeService for status lookups)
*/
function getWorktreeAutoLoopKey(projectPath: string, branchName: string | null): string {
const normalizedBranch = branchName === 'main' ? null : branchName;
return `${projectPath}::${normalizedBranch ?? '__main__'}`;
}
/**
* AutoModeServiceFacade provides a clean interface for auto-mode functionality.
*
* Created via factory pattern with a specific projectPath, allowing methods
* to use clean names without requiring projectPath as a parameter.
*/
export class AutoModeServiceFacade {
private constructor(
private readonly projectPath: string,
private readonly events: EventEmitter,
private readonly eventBus: TypedEventBus,
private readonly concurrencyManager: ConcurrencyManager,
private readonly worktreeResolver: WorktreeResolver,
private readonly featureStateManager: FeatureStateManager,
private readonly featureLoader: FeatureLoader,
private readonly planApprovalService: PlanApprovalService,
private readonly autoLoopCoordinator: AutoLoopCoordinator,
private readonly executionService: ExecutionService,
private readonly recoveryService: RecoveryService,
private readonly pipelineOrchestrator: PipelineOrchestrator,
private readonly settingsService: SettingsService | null
) {}
/**
* Create a new AutoModeServiceFacade instance for a specific project.
*
* @param projectPath - The project path this facade operates on
* @param options - Configuration options including events, settingsService, featureLoader
*/
static create(projectPath: string, options: FacadeOptions): AutoModeServiceFacade {
const { events, settingsService = null, featureLoader = new FeatureLoader() } = options;
// Create core services
const eventBus = new TypedEventBus(events);
const worktreeResolver = new WorktreeResolver();
const concurrencyManager = new ConcurrencyManager((p) => worktreeResolver.getCurrentBranch(p));
const featureStateManager = new FeatureStateManager(events, featureLoader);
const planApprovalService = new PlanApprovalService(
eventBus,
featureStateManager,
settingsService
);
const agentExecutor = new AgentExecutor(
eventBus,
featureStateManager,
planApprovalService,
settingsService
);
const testRunnerService = new TestRunnerService();
// Helper for building feature prompts (used by pipeline orchestrator)
const buildFeaturePrompt = (
feature: Feature,
prompts: { implementationInstructions: string; playwrightVerificationInstructions: string }
): string => {
const title =
feature.title || feature.description?.split('\n')[0]?.substring(0, 60) || 'Untitled';
let prompt = `## Feature Implementation Task\n\n**Feature ID:** ${feature.id}\n**Title:** ${title}\n**Description:** ${feature.description}\n`;
if (feature.spec) {
prompt += `\n**Specification:**\n${feature.spec}\n`;
}
if (!feature.skipTests) {
prompt += `\n${prompts.implementationInstructions}\n\n${prompts.playwrightVerificationInstructions}`;
} else {
prompt += `\n${prompts.implementationInstructions}`;
}
return prompt;
};
// Create placeholder callbacks - will be bound to facade methods after creation
// These use closures to capture the facade instance once created
let facadeInstance: AutoModeServiceFacade | null = null;
// PipelineOrchestrator - runAgentFn is a stub; routes use AutoModeService directly
const pipelineOrchestrator = new PipelineOrchestrator(
eventBus,
featureStateManager,
agentExecutor,
testRunnerService,
worktreeResolver,
concurrencyManager,
settingsService,
// Callbacks
(pPath, featureId, status) =>
featureStateManager.updateFeatureStatus(pPath, featureId, status),
loadContextFiles,
buildFeaturePrompt,
(pPath, featureId, useWorktrees, _isAutoMode, _model, opts) =>
facadeInstance!.executeFeature(featureId, useWorktrees, false, undefined, opts),
// runAgentFn stub - facade does not implement runAgent directly
async () => {
throw new Error('runAgentFn not implemented in facade');
}
);
// AutoLoopCoordinator
const autoLoopCoordinator = new AutoLoopCoordinator(
eventBus,
concurrencyManager,
settingsService,
// Callbacks
(pPath, featureId, useWorktrees, isAutoMode) =>
facadeInstance!.executeFeature(featureId, useWorktrees, isAutoMode),
(pPath, branchName) =>
featureLoader
.getAll(pPath)
.then((features) =>
features.filter(
(f) =>
(f.status === 'backlog' || f.status === 'ready') &&
(branchName === null
? !f.branchName || f.branchName === 'main'
: f.branchName === branchName)
)
),
(pPath, branchName, maxConcurrency) =>
facadeInstance!.saveExecutionStateForProject(branchName, maxConcurrency),
(pPath, branchName) => facadeInstance!.clearExecutionState(branchName),
(pPath) => featureStateManager.resetStuckFeatures(pPath),
(feature) =>
feature.status === 'completed' ||
feature.status === 'verified' ||
feature.status === 'waiting_approval',
(featureId) => concurrencyManager.isRunning(featureId)
);
// ExecutionService - runAgentFn is a stub
const executionService = new ExecutionService(
eventBus,
concurrencyManager,
worktreeResolver,
settingsService,
// Callbacks - runAgentFn stub
async () => {
throw new Error('runAgentFn not implemented in facade');
},
(context) => pipelineOrchestrator.executePipeline(context),
(pPath, featureId, status) =>
featureStateManager.updateFeatureStatus(pPath, featureId, status),
(pPath, featureId) => featureStateManager.loadFeature(pPath, featureId),
async (_feature) => {
// getPlanningPromptPrefixFn - planning prompts handled by AutoModeService
return '';
},
(pPath, featureId, summary) =>
featureStateManager.saveFeatureSummary(pPath, featureId, summary),
async () => {
/* recordLearnings - stub */
},
(pPath, featureId) => facadeInstance!.contextExists(featureId),
(pPath, featureId, useWorktrees, _calledInternally) =>
facadeInstance!.resumeFeature(featureId, useWorktrees, _calledInternally),
(errorInfo) =>
autoLoopCoordinator.trackFailureAndCheckPauseForProject(projectPath, errorInfo),
(errorInfo) => autoLoopCoordinator.signalShouldPauseForProject(projectPath, errorInfo),
() => {
/* recordSuccess - no-op */
},
(_pPath) => facadeInstance!.saveExecutionState(),
loadContextFiles
);
// RecoveryService
const recoveryService = new RecoveryService(
eventBus,
concurrencyManager,
settingsService,
// Callbacks
(pPath, featureId, useWorktrees, isAutoMode, providedWorktreePath, opts) =>
facadeInstance!.executeFeature(
featureId,
useWorktrees,
isAutoMode,
providedWorktreePath,
opts
),
(pPath, featureId) => featureStateManager.loadFeature(pPath, featureId),
(pPath, featureId, status) =>
pipelineOrchestrator.detectPipelineStatus(pPath, featureId, status),
(pPath, feature, useWorktrees, pipelineInfo) =>
pipelineOrchestrator.resumePipeline(pPath, feature, useWorktrees, pipelineInfo),
(featureId) => concurrencyManager.isRunning(featureId),
(opts) => concurrencyManager.acquire(opts),
(featureId) => concurrencyManager.release(featureId)
);
// Create the facade instance
facadeInstance = new AutoModeServiceFacade(
projectPath,
events,
eventBus,
concurrencyManager,
worktreeResolver,
featureStateManager,
featureLoader,
planApprovalService,
autoLoopCoordinator,
executionService,
recoveryService,
pipelineOrchestrator,
settingsService
);
return facadeInstance;
}
// ===========================================================================
// AUTO LOOP CONTROL (4 methods)
// ===========================================================================
/**
* Start the auto mode loop for this project/worktree
* @param branchName - The branch name for worktree scoping, null for main worktree
* @param maxConcurrency - Maximum concurrent features
*/
async startAutoLoop(branchName: string | null = null, maxConcurrency?: number): Promise<number> {
return this.autoLoopCoordinator.startAutoLoopForProject(
this.projectPath,
branchName,
maxConcurrency
);
}
/**
* Stop the auto mode loop for this project/worktree
* @param branchName - The branch name, or null for main worktree
*/
async stopAutoLoop(branchName: string | null = null): Promise<number> {
return this.autoLoopCoordinator.stopAutoLoopForProject(this.projectPath, branchName);
}
/**
* Check if auto mode is running for this project/worktree
* @param branchName - The branch name, or null for main worktree
*/
isAutoLoopRunning(branchName: string | null = null): boolean {
return this.autoLoopCoordinator.isAutoLoopRunningForProject(this.projectPath, branchName);
}
/**
* Get auto loop config for this project/worktree
* @param branchName - The branch name, or null for main worktree
*/
getAutoLoopConfig(branchName: string | null = null): AutoModeConfig | null {
return this.autoLoopCoordinator.getAutoLoopConfigForProject(this.projectPath, branchName);
}
// ===========================================================================
// FEATURE EXECUTION (6 methods)
// ===========================================================================
/**
* Execute a single feature
* @param featureId - The feature ID to execute
* @param useWorktrees - Whether to use worktrees for isolation
* @param isAutoMode - Whether this is running in auto mode
* @param providedWorktreePath - Optional pre-resolved worktree path
* @param options - Additional execution options
*/
async executeFeature(
featureId: string,
useWorktrees = false,
isAutoMode = false,
providedWorktreePath?: string,
options?: {
continuationPrompt?: string;
_calledInternally?: boolean;
}
): Promise<void> {
return this.executionService.executeFeature(
this.projectPath,
featureId,
useWorktrees,
isAutoMode,
providedWorktreePath,
options
);
}
/**
* Stop a specific feature
* @param featureId - ID of the feature to stop
*/
async stopFeature(featureId: string): Promise<boolean> {
// Cancel any pending plan approval for this feature
this.cancelPlanApproval(featureId);
return this.executionService.stopFeature(featureId);
}
/**
* Resume a feature (continues from saved context or starts fresh)
* @param featureId - ID of the feature to resume
* @param useWorktrees - Whether to use git worktrees
* @param _calledInternally - Internal flag for nested calls
*/
async resumeFeature(
featureId: string,
useWorktrees = false,
_calledInternally = false
): Promise<void> {
return this.recoveryService.resumeFeature(
this.projectPath,
featureId,
useWorktrees,
_calledInternally
);
}
/**
* Follow up on a feature with additional instructions
* @param featureId - The feature ID
* @param prompt - Follow-up prompt
* @param imagePaths - Optional image paths
* @param useWorktrees - Whether to use worktrees
*/
async followUpFeature(
featureId: string,
prompt: string,
imagePaths?: string[],
useWorktrees = true
): Promise<void> {
// This method contains substantial logic - delegates most work to AgentExecutor
validateWorkingDirectory(this.projectPath);
const runningEntry = this.concurrencyManager.acquire({
featureId,
projectPath: this.projectPath,
isAutoMode: false,
});
const abortController = runningEntry.abortController;
const feature = await this.featureStateManager.loadFeature(this.projectPath, featureId);
let workDir = path.resolve(this.projectPath);
let worktreePath: string | null = null;
const branchName = feature?.branchName || `feature/${featureId}`;
if (useWorktrees && branchName) {
worktreePath = await this.worktreeResolver.findWorktreeForBranch(
this.projectPath,
branchName
);
if (worktreePath) {
workDir = worktreePath;
}
}
// Load previous context
const featureDir = getFeatureDir(this.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
}
const prompts = await getPromptCustomization(this.settingsService, '[Facade]');
// Build follow-up prompt inline (no template in TaskExecutionPrompts)
let fullPrompt = `## Follow-up on Feature Implementation
${feature ? `**Feature ID:** ${feature.id}\n**Title:** ${feature.title || 'Untitled'}\n**Description:** ${feature.description}` : `**Feature ID:** ${featureId}`}
`;
if (previousContext) {
fullPrompt += `
## Previous Agent Work
The following is the output from the previous implementation attempt:
${previousContext}
`;
}
fullPrompt += `
## Follow-up Instructions
${prompt}
## Task
Address the follow-up instructions above. Review the previous work and make the requested changes or fixes.`;
try {
this.eventBus.emitAutoModeEvent('auto_mode_feature_start', {
featureId,
projectPath: this.projectPath,
branchName: feature?.branchName ?? null,
feature: {
id: featureId,
title: feature?.title || 'Follow-up',
description: feature?.description || 'Following up on feature',
},
});
// NOTE: Facade does not have runAgent - this method requires AutoModeService
// For now, throw to indicate routes should use AutoModeService.followUpFeature
throw new Error(
'followUpFeature not fully implemented in facade - use AutoModeService.followUpFeature instead'
);
} catch (error) {
const errorInfo = classifyError(error);
if (!errorInfo.isAbort) {
this.eventBus.emitAutoModeEvent('auto_mode_error', {
featureId,
featureName: feature?.title,
branchName: feature?.branchName ?? null,
error: errorInfo.message,
errorType: errorInfo.type,
projectPath: this.projectPath,
});
}
throw error;
} finally {
this.concurrencyManager.release(featureId);
}
}
/**
* Verify a feature's implementation
* @param featureId - The feature ID to verify
*/
async verifyFeature(featureId: string): Promise<boolean> {
const feature = await this.featureStateManager.loadFeature(this.projectPath, featureId);
const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-');
const worktreePath = path.join(this.projectPath, '.worktrees', sanitizedFeatureId);
let workDir = this.projectPath;
try {
await secureFs.access(worktreePath);
workDir = worktreePath;
} catch {
// No worktree
}
const verificationChecks = [
{ cmd: 'npm run lint', name: 'Lint' },
{ cmd: 'npm run typecheck', name: 'Type check' },
{ cmd: 'npm test', name: 'Tests' },
{ cmd: 'npm run build', name: 'Build' },
];
let allPassed = true;
const results: Array<{ check: string; passed: boolean; output?: string }> = [];
for (const check of verificationChecks) {
try {
const { stdout, stderr } = await execAsync(check.cmd, { cwd: workDir, timeout: 120000 });
results.push({ check: check.name, passed: true, output: stdout || stderr });
} catch (error) {
allPassed = false;
results.push({ check: check.name, passed: false, output: (error as Error).message });
break;
}
}
this.eventBus.emitAutoModeEvent('auto_mode_feature_complete', {
featureId,
featureName: feature?.title,
branchName: feature?.branchName ?? null,
passes: allPassed,
message: allPassed
? 'All verification checks passed'
: `Verification failed: ${results.find((r) => !r.passed)?.check || 'Unknown'}`,
projectPath: this.projectPath,
});
return allPassed;
}
/**
* Commit feature changes
* @param featureId - The feature ID to commit
* @param providedWorktreePath - Optional worktree path
*/
async commitFeature(featureId: string, providedWorktreePath?: string): Promise<string | null> {
let workDir = this.projectPath;
if (providedWorktreePath) {
try {
await secureFs.access(providedWorktreePath);
workDir = providedWorktreePath;
} catch {
// Use project path
}
} else {
const sanitizedFeatureId = featureId.replace(/[^a-zA-Z0-9_-]/g, '-');
const legacyWorktreePath = path.join(this.projectPath, '.worktrees', sanitizedFeatureId);
try {
await secureFs.access(legacyWorktreePath);
workDir = legacyWorktreePath;
} catch {
// Use project path
}
}
try {
const { stdout: status } = await execAsync('git status --porcelain', { cwd: workDir });
if (!status.trim()) {
return null;
}
const feature = await this.featureStateManager.loadFeature(this.projectPath, featureId);
const title =
feature?.description?.split('\n')[0]?.substring(0, 60) || `Feature ${featureId}`;
const commitMessage = `feat: ${title}\n\nImplemented by Automaker auto-mode`;
await execAsync('git add -A', { cwd: workDir });
await execAsync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { cwd: workDir });
const { stdout: hash } = await execAsync('git rev-parse HEAD', { cwd: workDir });
this.eventBus.emitAutoModeEvent('auto_mode_feature_complete', {
featureId,
featureName: feature?.title,
branchName: feature?.branchName ?? null,
passes: true,
message: `Changes committed: ${hash.trim().substring(0, 8)}`,
projectPath: this.projectPath,
});
return hash.trim();
} catch (error) {
logger.error(`Commit failed for ${featureId}:`, error);
return null;
}
}
// ===========================================================================
// STATUS AND QUERIES (7 methods)
// ===========================================================================
/**
* Get current status (global across all projects)
*/
getStatus(): AutoModeStatus {
const allRunning = this.concurrencyManager.getAllRunning();
return {
isRunning: allRunning.length > 0,
runningFeatures: allRunning.map((rf) => rf.featureId),
runningCount: allRunning.length,
};
}
/**
* Get status for this project/worktree
* @param branchName - The branch name, or null for main worktree
*/
getStatusForProject(branchName: string | null = null): ProjectAutoModeStatus {
const isAutoLoopRunning = this.autoLoopCoordinator.isAutoLoopRunningForProject(
this.projectPath,
branchName
);
const config = this.autoLoopCoordinator.getAutoLoopConfigForProject(
this.projectPath,
branchName
);
const runningFeatures = this.concurrencyManager
.getAllRunning()
.filter((f) => f.projectPath === this.projectPath && f.branchName === branchName)
.map((f) => f.featureId);
return {
isAutoLoopRunning,
runningFeatures,
runningCount: runningFeatures.length,
maxConcurrency: config?.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY,
branchName,
};
}
/**
* Get all active auto loop projects (unique project paths)
*/
getActiveAutoLoopProjects(): string[] {
// This needs access to internal state - for now return empty
// Routes should migrate to getActiveAutoLoopWorktrees
return [];
}
/**
* Get all active auto loop worktrees
*/
getActiveAutoLoopWorktrees(): Array<{ projectPath: string; branchName: string | null }> {
// This needs access to internal state - for now return empty
// Will be properly implemented when routes migrate
return [];
}
/**
* Get detailed info about all running agents
*/
async getRunningAgents(): Promise<RunningAgentInfo[]> {
const agents = await Promise.all(
this.concurrencyManager.getAllRunning().map(async (rf) => {
let title: string | undefined;
let description: string | undefined;
let branchName: string | undefined;
try {
const feature = await this.featureLoader.get(rf.projectPath, rf.featureId);
if (feature) {
title = feature.title;
description = feature.description;
branchName = feature.branchName;
}
} catch {
// Silently ignore
}
return {
featureId: rf.featureId,
projectPath: rf.projectPath,
projectName: path.basename(rf.projectPath),
isAutoMode: rf.isAutoMode,
model: rf.model,
provider: rf.provider,
title,
description,
branchName,
};
})
);
return agents;
}
/**
* Check if there's capacity to start a feature on a worktree
* @param featureId - The feature ID to check capacity for
*/
async checkWorktreeCapacity(featureId: string): Promise<WorktreeCapacityInfo> {
const feature = await this.featureStateManager.loadFeature(this.projectPath, featureId);
const rawBranchName = feature?.branchName ?? null;
const branchName = rawBranchName === 'main' ? null : rawBranchName;
const maxAgents = await this.autoLoopCoordinator.resolveMaxConcurrency(
this.projectPath,
branchName
);
const currentAgents = await this.concurrencyManager.getRunningCountForWorktree(
this.projectPath,
branchName
);
return {
hasCapacity: currentAgents < maxAgents,
currentAgents,
maxAgents,
branchName,
};
}
/**
* Check if context exists for a feature
* @param featureId - The feature ID
*/
async contextExists(featureId: string): Promise<boolean> {
return this.recoveryService.contextExists(this.projectPath, featureId);
}
// ===========================================================================
// PLAN APPROVAL (4 methods)
// ===========================================================================
/**
* Resolve a pending plan approval
* @param featureId - The feature ID
* @param approved - Whether the plan was approved
* @param editedPlan - Optional edited plan content
* @param feedback - Optional feedback
*/
async resolvePlanApproval(
featureId: string,
approved: boolean,
editedPlan?: string,
feedback?: string
): Promise<{ success: boolean; error?: string }> {
const result = await this.planApprovalService.resolveApproval(featureId, approved, {
editedPlan,
feedback,
projectPath: this.projectPath,
});
// Handle recovery case
if (result.success && result.needsRecovery) {
const feature = await this.featureStateManager.loadFeature(this.projectPath, featureId);
if (feature) {
const prompts = await getPromptCustomization(this.settingsService, '[Facade]');
const planContent = editedPlan || feature.planSpec?.content || '';
let continuationPrompt = prompts.taskExecution.continuationAfterApprovalTemplate;
continuationPrompt = continuationPrompt.replace(/\{\{userFeedback\}\}/g, feedback || '');
continuationPrompt = continuationPrompt.replace(/\{\{approvedPlan\}\}/g, planContent);
// Start execution async
this.executeFeature(featureId, true, false, undefined, { continuationPrompt }).catch(
(error) => {
logger.error(`Recovery execution failed for feature ${featureId}:`, error);
}
);
}
}
return { success: result.success, error: result.error };
}
/**
* Wait for plan approval
* @param featureId - The feature ID
*/
waitForPlanApproval(
featureId: string
): Promise<{ approved: boolean; editedPlan?: string; feedback?: string }> {
return this.planApprovalService.waitForApproval(featureId, this.projectPath);
}
/**
* Check if a feature has a pending plan approval
* @param featureId - The feature ID
*/
hasPendingApproval(featureId: string): boolean {
return this.planApprovalService.hasPendingApproval(featureId);
}
/**
* Cancel a pending plan approval
* @param featureId - The feature ID
*/
cancelPlanApproval(featureId: string): void {
this.planApprovalService.cancelApproval(featureId);
}
// ===========================================================================
// ANALYSIS AND RECOVERY (3 methods)
// ===========================================================================
/**
* Analyze project to gather context
*
* NOTE: This method requires complex provider integration that is only available
* in AutoModeService. The facade exposes the method signature for API compatibility,
* but routes should use AutoModeService.analyzeProject() until migration is complete.
*/
async analyzeProject(): Promise<void> {
// analyzeProject requires provider.execute which is complex to wire up
// For now, throw to indicate routes should use AutoModeService
throw new Error(
'analyzeProject not fully implemented in facade - use AutoModeService.analyzeProject instead'
);
}
/**
* Resume interrupted features after server restart
*/
async resumeInterruptedFeatures(): Promise<void> {
return this.recoveryService.resumeInterruptedFeatures(this.projectPath);
}
/**
* Detect orphaned features (features with missing branches)
*/
async detectOrphanedFeatures(): Promise<OrphanedFeatureInfo[]> {
const orphanedFeatures: OrphanedFeatureInfo[] = [];
try {
const allFeatures = await this.featureLoader.getAll(this.projectPath);
const featuresWithBranches = allFeatures.filter(
(f) => f.branchName && f.branchName.trim() !== ''
);
if (featuresWithBranches.length === 0) {
return orphanedFeatures;
}
// Get existing branches
const { stdout } = await execAsync(
'git for-each-ref --format="%(refname:short)" refs/heads/',
{ cwd: this.projectPath }
);
const existingBranches = new Set(
stdout
.trim()
.split('\n')
.map((b) => b.trim())
.filter(Boolean)
);
const primaryBranch = await this.worktreeResolver.getCurrentBranch(this.projectPath);
for (const feature of featuresWithBranches) {
const branchName = feature.branchName!;
if (primaryBranch && branchName === primaryBranch) {
continue;
}
if (!existingBranches.has(branchName)) {
orphanedFeatures.push({ feature, missingBranch: branchName });
}
}
return orphanedFeatures;
} catch (error) {
logger.error('[detectOrphanedFeatures] Error:', error);
return orphanedFeatures;
}
}
// ===========================================================================
// LIFECYCLE (1 method)
// ===========================================================================
/**
* Mark all running features as interrupted
* @param reason - Optional reason for the interruption
*/
async markAllRunningFeaturesInterrupted(reason?: string): Promise<void> {
const allRunning = this.concurrencyManager.getAllRunning();
for (const rf of allRunning) {
await this.featureStateManager.markFeatureInterrupted(rf.projectPath, rf.featureId, reason);
}
if (allRunning.length > 0) {
logger.info(
`Marked ${allRunning.length} running feature(s) as interrupted: ${reason || 'no reason provided'}`
);
}
}
// ===========================================================================
// INTERNAL HELPERS
// ===========================================================================
/**
* Save execution state for recovery
*/
private async saveExecutionState(): Promise<void> {
return this.saveExecutionStateForProject(null, DEFAULT_MAX_CONCURRENCY);
}
/**
* Save execution state for a specific worktree
*/
private async saveExecutionStateForProject(
branchName: string | null,
maxConcurrency: number
): Promise<void> {
return this.recoveryService.saveExecutionStateForProject(
this.projectPath,
branchName,
maxConcurrency
);
}
/**
* Clear execution state
*/
private async clearExecutionState(branchName: string | null = null): Promise<void> {
return this.recoveryService.clearExecutionState(this.projectPath, branchName);
}
}