diff --git a/apps/server/src/routes/auto-mode/index.ts b/apps/server/src/routes/auto-mode/index.ts index e587a061..de376b7d 100644 --- a/apps/server/src/routes/auto-mode/index.ts +++ b/apps/server/src/routes/auto-mode/index.ts @@ -1,11 +1,13 @@ /** * Auto Mode routes - HTTP API for autonomous feature implementation * - * Uses the AutoModeService for real feature execution with Claude Agent SDK + * Uses the AutoModeService for real feature execution with Claude Agent SDK. + * Supports optional facadeFactory for per-project facade creation during migration. */ import { Router } from 'express'; import type { AutoModeService } from '../../services/auto-mode-service.js'; +import type { AutoModeServiceFacade } from '../../services/auto-mode/index.js'; import { validatePathParams } from '../../middleware/validate-paths.js'; import { createStopFeatureHandler } from './routes/stop-feature.js'; import { createStatusHandler } from './routes/status.js'; @@ -21,59 +23,82 @@ import { createCommitFeatureHandler } from './routes/commit-feature.js'; import { createApprovePlanHandler } from './routes/approve-plan.js'; import { createResumeInterruptedHandler } from './routes/resume-interrupted.js'; -export function createAutoModeRoutes(autoModeService: AutoModeService): Router { +/** + * Create auto-mode routes with optional facade factory. + * + * @param autoModeService - The AutoModeService instance (for backward compatibility) + * @param facadeFactory - Optional factory for creating per-project facades + */ +export function createAutoModeRoutes( + autoModeService: AutoModeService, + facadeFactory?: (projectPath: string) => AutoModeServiceFacade +): Router { const router = Router(); // Auto loop control routes - router.post('/start', validatePathParams('projectPath'), createStartHandler(autoModeService)); - router.post('/stop', validatePathParams('projectPath'), createStopHandler(autoModeService)); + router.post( + '/start', + validatePathParams('projectPath'), + createStartHandler(autoModeService, facadeFactory) + ); + router.post( + '/stop', + validatePathParams('projectPath'), + createStopHandler(autoModeService, facadeFactory) + ); + // Note: stop-feature doesn't have projectPath, so we pass undefined for facade. + // When we fully migrate, we can update stop-feature to use a different approach. router.post('/stop-feature', createStopFeatureHandler(autoModeService)); - router.post('/status', validatePathParams('projectPath?'), createStatusHandler(autoModeService)); + router.post( + '/status', + validatePathParams('projectPath?'), + createStatusHandler(autoModeService, facadeFactory) + ); router.post( '/run-feature', validatePathParams('projectPath'), - createRunFeatureHandler(autoModeService) + createRunFeatureHandler(autoModeService, facadeFactory) ); router.post( '/verify-feature', validatePathParams('projectPath'), - createVerifyFeatureHandler(autoModeService) + createVerifyFeatureHandler(autoModeService, facadeFactory) ); router.post( '/resume-feature', validatePathParams('projectPath'), - createResumeFeatureHandler(autoModeService) + createResumeFeatureHandler(autoModeService, facadeFactory) ); router.post( '/context-exists', validatePathParams('projectPath'), - createContextExistsHandler(autoModeService) + createContextExistsHandler(autoModeService, facadeFactory) ); router.post( '/analyze-project', validatePathParams('projectPath'), - createAnalyzeProjectHandler(autoModeService) + createAnalyzeProjectHandler(autoModeService, facadeFactory) ); router.post( '/follow-up-feature', validatePathParams('projectPath', 'imagePaths[]'), - createFollowUpFeatureHandler(autoModeService) + createFollowUpFeatureHandler(autoModeService, facadeFactory) ); router.post( '/commit-feature', validatePathParams('projectPath', 'worktreePath?'), - createCommitFeatureHandler(autoModeService) + createCommitFeatureHandler(autoModeService, facadeFactory) ); router.post( '/approve-plan', validatePathParams('projectPath'), - createApprovePlanHandler(autoModeService) + createApprovePlanHandler(autoModeService, facadeFactory) ); router.post( '/resume-interrupted', validatePathParams('projectPath'), - createResumeInterruptedHandler(autoModeService) + createResumeInterruptedHandler(autoModeService, facadeFactory) ); return router; diff --git a/apps/server/src/routes/features/index.ts b/apps/server/src/routes/features/index.ts index 8c7dbb06..bc0f0b52 100644 --- a/apps/server/src/routes/features/index.ts +++ b/apps/server/src/routes/features/index.ts @@ -6,6 +6,7 @@ import { Router } from 'express'; import { FeatureLoader } from '../../services/feature-loader.js'; import type { SettingsService } from '../../services/settings-service.js'; import type { AutoModeService } from '../../services/auto-mode-service.js'; +import type { AutoModeServiceFacade } from '../../services/auto-mode/index.js'; import type { EventEmitter } from '../../lib/events.js'; import { validatePathParams } from '../../middleware/validate-paths.js'; import { createListHandler } from './routes/list.js'; @@ -24,14 +25,15 @@ export function createFeaturesRoutes( featureLoader: FeatureLoader, settingsService?: SettingsService, events?: EventEmitter, - autoModeService?: AutoModeService + autoModeService?: AutoModeService, + facadeFactory?: (projectPath: string) => AutoModeServiceFacade ): Router { const router = Router(); router.post( '/list', validatePathParams('projectPath'), - createListHandler(featureLoader, autoModeService) + createListHandler(featureLoader, autoModeService, facadeFactory) ); router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader)); router.post( diff --git a/apps/server/src/routes/features/routes/list.ts b/apps/server/src/routes/features/routes/list.ts index 40c35966..1dc8f29f 100644 --- a/apps/server/src/routes/features/routes/list.ts +++ b/apps/server/src/routes/features/routes/list.ts @@ -8,12 +8,17 @@ import type { Request, Response } from 'express'; import { FeatureLoader } from '../../../services/feature-loader.js'; import type { AutoModeService } from '../../../services/auto-mode-service.js'; +import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js'; import { getErrorMessage, logError } from '../common.js'; import { createLogger } from '@automaker/utils'; const logger = createLogger('FeaturesListRoute'); -export function createListHandler(featureLoader: FeatureLoader, autoModeService?: AutoModeService) { +export function createListHandler( + featureLoader: FeatureLoader, + autoModeService?: AutoModeService, + facadeFactory?: (projectPath: string) => AutoModeServiceFacade +) { return async (req: Request, res: Response): Promise => { try { const { projectPath } = req.body as { projectPath: string }; @@ -29,7 +34,21 @@ export function createListHandler(featureLoader: FeatureLoader, autoModeService? // This detects features whose branches no longer exist (e.g., after merge/delete) // We don't await this to keep the list response fast // Note: detectOrphanedFeatures handles errors internally and always resolves - if (autoModeService) { + if (facadeFactory) { + const facade = facadeFactory(projectPath); + facade.detectOrphanedFeatures().then((orphanedFeatures) => { + if (orphanedFeatures.length > 0) { + logger.info( + `[ProjectLoad] Detected ${orphanedFeatures.length} orphaned feature(s) in ${projectPath}` + ); + for (const { feature, missingBranch } of orphanedFeatures) { + logger.info( + `[ProjectLoad] Orphaned: ${feature.title || feature.id} - branch "${missingBranch}" no longer exists` + ); + } + } + }); + } else if (autoModeService) { autoModeService.detectOrphanedFeatures(projectPath).then((orphanedFeatures) => { if (orphanedFeatures.length > 0) { logger.info( diff --git a/apps/server/src/routes/projects/index.ts b/apps/server/src/routes/projects/index.ts index 24ecef14..f698e76a 100644 --- a/apps/server/src/routes/projects/index.ts +++ b/apps/server/src/routes/projects/index.ts @@ -5,6 +5,7 @@ import { Router } from 'express'; import type { FeatureLoader } from '../../services/feature-loader.js'; import type { AutoModeService } from '../../services/auto-mode-service.js'; +import type { AutoModeServiceFacade } from '../../services/auto-mode/index.js'; import type { SettingsService } from '../../services/settings-service.js'; import type { NotificationService } from '../../services/notification-service.js'; import { createOverviewHandler } from './routes/overview.js'; @@ -13,14 +14,21 @@ export function createProjectsRoutes( featureLoader: FeatureLoader, autoModeService: AutoModeService, settingsService: SettingsService, - notificationService: NotificationService + notificationService: NotificationService, + facadeFactory?: (projectPath: string) => AutoModeServiceFacade ): Router { const router = Router(); // GET /overview - Get aggregate status for all projects router.get( '/overview', - createOverviewHandler(featureLoader, autoModeService, settingsService, notificationService) + createOverviewHandler( + featureLoader, + autoModeService, + settingsService, + notificationService, + facadeFactory + ) ); return router; diff --git a/apps/server/src/routes/projects/routes/overview.ts b/apps/server/src/routes/projects/routes/overview.ts index e58c9c0c..a5cf0819 100644 --- a/apps/server/src/routes/projects/routes/overview.ts +++ b/apps/server/src/routes/projects/routes/overview.ts @@ -10,6 +10,11 @@ import type { Request, Response } from 'express'; import type { FeatureLoader } from '../../../services/feature-loader.js'; import type { AutoModeService } from '../../../services/auto-mode-service.js'; +import type { + AutoModeServiceFacade, + RunningAgentInfo, + ProjectAutoModeStatus, +} from '../../../services/auto-mode/index.js'; import type { SettingsService } from '../../../services/settings-service.js'; import type { NotificationService } from '../../../services/notification-service.js'; import type { @@ -149,7 +154,8 @@ export function createOverviewHandler( featureLoader: FeatureLoader, autoModeService: AutoModeService, settingsService: SettingsService, - notificationService: NotificationService + notificationService: NotificationService, + facadeFactory?: (projectPath: string) => AutoModeServiceFacade ) { return async (_req: Request, res: Response): Promise => { try { @@ -158,7 +164,15 @@ export function createOverviewHandler( const projectRefs: ProjectRef[] = settings.projects || []; // Get all running agents once to count live running features per project - const allRunningAgents = await autoModeService.getRunningAgents(); + // Use facade if available, otherwise fall back to autoModeService + let allRunningAgents: RunningAgentInfo[]; + if (facadeFactory && projectRefs.length > 0) { + // For running agents, we can use any project's facade since it's a global query + const facade = facadeFactory(projectRefs[0].path); + allRunningAgents = await facade.getRunningAgents(); + } else { + allRunningAgents = await autoModeService.getRunningAgents(); + } // Collect project statuses in parallel const projectStatusPromises = projectRefs.map(async (projectRef): Promise => { @@ -169,7 +183,13 @@ export function createOverviewHandler( const totalFeatures = features.length; // Get auto-mode status for this project (main worktree, branchName = null) - const autoModeStatus = autoModeService.getStatusForProject(projectRef.path, null); + let autoModeStatus: ProjectAutoModeStatus; + if (facadeFactory) { + const facade = facadeFactory(projectRef.path); + autoModeStatus = facade.getStatusForProject(null); + } else { + autoModeStatus = autoModeService.getStatusForProject(projectRef.path, null); + } const isAutoModeRunning = autoModeStatus.isAutoLoopRunning; // Count live running features for this project (across all branches)