refactor(06-04): delete auto-mode-service.ts monolith

- Delete the 2705-line auto-mode-service.ts monolith
- Create AutoModeServiceCompat as compatibility layer for routes
- Create GlobalAutoModeService for cross-project operations
- Update all routes to use AutoModeServiceCompat type
- Add SharedServices interface for state sharing across facades
- Add getActiveProjects/getActiveWorktrees to AutoLoopCoordinator
- Delete obsolete monolith test files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-30 22:05:46 +01:00
committed by gsxdsm
parent 7fd3d61a59
commit 473f935c90
31 changed files with 624 additions and 5180 deletions

View File

@@ -1,13 +1,12 @@
/**
* Auto Mode routes - HTTP API for autonomous feature implementation
*
* Uses the AutoModeService for real feature execution with Claude Agent SDK.
* Supports optional facadeFactory for per-project facade creation during migration.
* Uses AutoModeServiceCompat which provides the old interface while
* delegating to GlobalAutoModeService and per-project facades.
*/
import { Router } from 'express';
import type { AutoModeService } from '../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } 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';
@@ -24,81 +23,63 @@ import { createApprovePlanHandler } from './routes/approve-plan.js';
import { createResumeInterruptedHandler } from './routes/resume-interrupted.js';
/**
* Create auto-mode routes with optional facade factory.
* Create auto-mode routes.
*
* @param autoModeService - The AutoModeService instance (for backward compatibility)
* @param facadeFactory - Optional factory for creating per-project facades
* @param autoModeService - AutoModeServiceCompat instance
*/
export function createAutoModeRoutes(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
): Router {
export function createAutoModeRoutes(autoModeService: AutoModeServiceCompat): Router {
const router = Router();
// Auto loop control routes
router.post(
'/start',
validatePathParams('projectPath'),
createStartHandler(autoModeService, facadeFactory)
);
router.post(
'/stop',
validatePathParams('projectPath'),
createStopHandler(autoModeService, facadeFactory)
);
router.post('/start', validatePathParams('projectPath'), createStartHandler(autoModeService));
router.post('/stop', validatePathParams('projectPath'), createStopHandler(autoModeService));
// 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, facadeFactory)
);
router.post('/status', validatePathParams('projectPath?'), createStatusHandler(autoModeService));
router.post(
'/run-feature',
validatePathParams('projectPath'),
createRunFeatureHandler(autoModeService, facadeFactory)
createRunFeatureHandler(autoModeService)
);
router.post(
'/verify-feature',
validatePathParams('projectPath'),
createVerifyFeatureHandler(autoModeService, facadeFactory)
createVerifyFeatureHandler(autoModeService)
);
router.post(
'/resume-feature',
validatePathParams('projectPath'),
createResumeFeatureHandler(autoModeService, facadeFactory)
createResumeFeatureHandler(autoModeService)
);
router.post(
'/context-exists',
validatePathParams('projectPath'),
createContextExistsHandler(autoModeService, facadeFactory)
createContextExistsHandler(autoModeService)
);
router.post(
'/analyze-project',
validatePathParams('projectPath'),
createAnalyzeProjectHandler(autoModeService, facadeFactory)
createAnalyzeProjectHandler(autoModeService)
);
router.post(
'/follow-up-feature',
validatePathParams('projectPath', 'imagePaths[]'),
createFollowUpFeatureHandler(autoModeService, facadeFactory)
createFollowUpFeatureHandler(autoModeService)
);
router.post(
'/commit-feature',
validatePathParams('projectPath', 'worktreePath?'),
createCommitFeatureHandler(autoModeService, facadeFactory)
createCommitFeatureHandler(autoModeService)
);
router.post(
'/approve-plan',
validatePathParams('projectPath'),
createApprovePlanHandler(autoModeService, facadeFactory)
createApprovePlanHandler(autoModeService)
);
router.post(
'/resume-interrupted',
validatePathParams('projectPath'),
createResumeInterruptedHandler(autoModeService, facadeFactory)
createResumeInterruptedHandler(autoModeService)
);
return router;

View File

@@ -3,17 +3,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('AutoMode');
export function createAnalyzeProjectHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createAnalyzeProjectHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath: string };
@@ -23,19 +19,6 @@ export function createAnalyzeProjectHandler(
return;
}
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
// Start analysis in background
facade.analyzeProject().catch((error) => {
logger.error(`[AutoMode] Project analysis error:`, error);
});
res.json({ success: true, message: 'Project analysis started' });
return;
}
// Legacy path: use autoModeService directly
// Start analysis in background
autoModeService.analyzeProject(projectPath).catch((error) => {
logger.error(`[AutoMode] Project analysis error:`, error);

View File

@@ -3,17 +3,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('AutoMode');
export function createApprovePlanHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createApprovePlanHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { featureId, approved, editedPlan, feedback, projectPath } = req.body as {
@@ -50,37 +46,13 @@ export function createApprovePlanHandler(
}${feedback ? ` - Feedback: ${feedback}` : ''}`
);
// Use facade if factory is provided and projectPath is available
if (facadeFactory && projectPath) {
const facade = facadeFactory(projectPath);
const result = await facade.resolvePlanApproval(featureId, approved, editedPlan, feedback);
if (!result.success) {
res.status(500).json({
success: false,
error: result.error,
});
return;
}
res.json({
success: true,
approved,
message: approved
? 'Plan approved - implementation will continue'
: 'Plan rejected - feature execution stopped',
});
return;
}
// Legacy path: use autoModeService directly
// Resolve the pending approval (with recovery support)
const result = await autoModeService.resolvePlanApproval(
projectPath || '',
featureId,
approved,
editedPlan,
feedback,
projectPath
feedback
);
if (!result.success) {

View File

@@ -3,18 +3,10 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create commit feature handler with transition compatibility.
* Accepts either autoModeService (legacy) or facadeFactory (new).
*/
export function createCommitFeatureHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createCommitFeatureHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, worktreePath } = req.body as {
@@ -31,15 +23,6 @@ export function createCommitFeatureHandler(
return;
}
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
const commitHash = await facade.commitFeature(featureId, worktreePath);
res.json({ success: true, commitHash });
return;
}
// Legacy path: use autoModeService directly
const commitHash = await autoModeService.commitFeature(projectPath, featureId, worktreePath);
res.json({ success: true, commitHash });
} catch (error) {

View File

@@ -3,18 +3,10 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create context exists handler with transition compatibility.
* Accepts either autoModeService (legacy) or facadeFactory (new).
*/
export function createContextExistsHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createContextExistsHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId } = req.body as {
@@ -30,15 +22,6 @@ export function createContextExistsHandler(
return;
}
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
const exists = await facade.contextExists(featureId);
res.json({ success: true, exists });
return;
}
// Legacy path: use autoModeService directly
const exists = await autoModeService.contextExists(projectPath, featureId);
res.json({ success: true, exists });
} catch (error) {

View File

@@ -3,17 +3,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('AutoMode');
export function createFollowUpFeatureHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createFollowUpFeatureHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, prompt, imagePaths, useWorktrees } = req.body as {
@@ -32,40 +28,14 @@ export function createFollowUpFeatureHandler(
return;
}
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
// Start follow-up in background
// followUpFeature derives workDir from feature.branchName
facade
// Default to false to match run-feature/resume-feature behavior.
// Worktrees should only be used when explicitly enabled by the user.
.followUpFeature(featureId, prompt, imagePaths, useWorktrees ?? false)
.catch((error) => {
logger.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when follow-up completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });
return;
}
// Legacy path: use autoModeService directly
// Start follow-up in background
// followUpFeature derives workDir from feature.branchName
// Default to false to match run-feature/resume-feature behavior.
// Worktrees should only be used when explicitly enabled by the user.
autoModeService
// Default to false to match run-feature/resume-feature behavior.
// Worktrees should only be used when explicitly enabled by the user.
.followUpFeature(projectPath, featureId, prompt, imagePaths, useWorktrees ?? false)
.catch((error) => {
logger.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when follow-up completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });

View File

@@ -3,17 +3,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('AutoMode');
export function createResumeFeatureHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createResumeFeatureHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, useWorktrees } = req.body as {
@@ -30,20 +26,6 @@ export function createResumeFeatureHandler(
return;
}
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
// Start resume in background
// Default to false - worktrees should only be used when explicitly enabled
facade.resumeFeature(featureId, useWorktrees ?? false).catch((error) => {
logger.error(`Resume feature ${featureId} error:`, error);
});
res.json({ success: true });
return;
}
// Legacy path: use autoModeService directly
// Start resume in background
// Default to false - worktrees should only be used when explicitly enabled
autoModeService

View File

@@ -7,8 +7,7 @@
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
const logger = createLogger('ResumeInterrupted');
@@ -16,10 +15,7 @@ interface ResumeInterruptedRequest {
projectPath: string;
}
export function createResumeInterruptedHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createResumeInterruptedHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
const { projectPath } = req.body as ResumeInterruptedRequest;
@@ -31,13 +27,7 @@ export function createResumeInterruptedHandler(
logger.info(`Checking for interrupted features in ${projectPath}`);
try {
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
await facade.resumeInterruptedFeatures();
} else {
await autoModeService.resumeInterruptedFeatures(projectPath);
}
await autoModeService.resumeInterruptedFeatures(projectPath);
res.json({
success: true,

View File

@@ -3,17 +3,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('AutoMode');
export function createRunFeatureHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createRunFeatureHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, useWorktrees } = req.body as {
@@ -30,45 +26,6 @@ export function createRunFeatureHandler(
return;
}
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
// Check per-worktree capacity before starting
const capacity = await facade.checkWorktreeCapacity(featureId);
if (!capacity.hasCapacity) {
const worktreeDesc = capacity.branchName
? `worktree "${capacity.branchName}"`
: 'main worktree';
res.status(429).json({
success: false,
error: `Agent limit reached for ${worktreeDesc} (${capacity.currentAgents}/${capacity.maxAgents}). Wait for running tasks to complete or increase the limit.`,
details: {
currentAgents: capacity.currentAgents,
maxAgents: capacity.maxAgents,
branchName: capacity.branchName,
},
});
return;
}
// Start execution in background
// executeFeature derives workDir from feature.branchName
facade
.executeFeature(featureId, useWorktrees ?? false, false)
.catch((error) => {
logger.error(`Feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when execution completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });
return;
}
// Legacy path: use autoModeService directly
// Check per-worktree capacity before starting
const capacity = await autoModeService.checkWorktreeCapacity(projectPath, featureId);
if (!capacity.hasCapacity) {
@@ -93,10 +50,6 @@ export function createRunFeatureHandler(
.executeFeature(projectPath, featureId, useWorktrees ?? false, false)
.catch((error) => {
logger.error(`Feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when execution completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });

View File

@@ -3,17 +3,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('AutoMode');
export function createStartHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createStartHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, branchName, maxConcurrency } = req.body as {
@@ -36,40 +32,6 @@ export function createStartHandler(
? `worktree ${normalizedBranchName}`
: 'main worktree';
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
// Check if already running
if (facade.isAutoLoopRunning(normalizedBranchName)) {
res.json({
success: true,
message: `Auto mode is already running for ${worktreeDesc}`,
alreadyRunning: true,
branchName: normalizedBranchName,
});
return;
}
// Start the auto loop for this project/worktree
const resolvedMaxConcurrency = await facade.startAutoLoop(
normalizedBranchName,
maxConcurrency
);
logger.info(
`Started auto loop for ${worktreeDesc} in project: ${projectPath} with maxConcurrency: ${resolvedMaxConcurrency}`
);
res.json({
success: true,
message: `Auto mode started with max ${resolvedMaxConcurrency} concurrent features`,
branchName: normalizedBranchName,
});
return;
}
// Legacy path: use autoModeService directly
// Check if already running
if (autoModeService.isAutoLoopRunningForProject(projectPath, normalizedBranchName)) {
res.json({

View File

@@ -6,19 +6,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create status handler with transition compatibility.
* Accepts either autoModeService (legacy) or facade (new).
* When facade is provided, creates a per-project facade for the request.
* Create status handler.
*/
export function createStatusHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createStatusHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, branchName } = req.body as {
@@ -31,24 +25,6 @@ export function createStatusHandler(
// Normalize branchName: undefined becomes null
const normalizedBranchName = branchName ?? null;
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
const projectStatus = facade.getStatusForProject(normalizedBranchName);
res.json({
success: true,
isRunning: projectStatus.runningCount > 0,
isAutoLoopRunning: projectStatus.isAutoLoopRunning,
runningFeatures: projectStatus.runningFeatures,
runningCount: projectStatus.runningCount,
maxConcurrency: projectStatus.maxConcurrency,
projectPath,
branchName: normalizedBranchName,
});
return;
}
// Legacy path: use autoModeService directly
const projectStatus = autoModeService.getStatusForProject(
projectPath,
normalizedBranchName
@@ -66,8 +42,7 @@ export function createStatusHandler(
return;
}
// Fall back to global status for backward compatibility
// Global status uses autoModeService (facade is per-project)
// Global status for backward compatibility
const status = autoModeService.getStatus();
const activeProjects = autoModeService.getActiveAutoLoopProjects();
const activeWorktrees = autoModeService.getActiveAutoLoopWorktrees();

View File

@@ -3,19 +3,10 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create stop feature handler with transition compatibility.
* Accepts either autoModeService (legacy) or facade (new).
* Note: stopFeature is feature-scoped (not project-scoped), so a single facade can be used.
*/
export function createStopFeatureHandler(
autoModeService: AutoModeService,
facade?: AutoModeServiceFacade
) {
export function createStopFeatureHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { featureId } = req.body as { featureId: string };
@@ -25,10 +16,7 @@ export function createStopFeatureHandler(
return;
}
// Use facade if provided, otherwise fall back to autoModeService
const stopped = facade
? await facade.stopFeature(featureId)
: await autoModeService.stopFeature(featureId);
const stopped = await autoModeService.stopFeature(featureId);
res.json({ success: true, stopped });
} catch (error) {
logError(error, 'Stop feature failed');

View File

@@ -3,21 +3,13 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('AutoMode');
/**
* Create stop handler with transition compatibility.
* Accepts either autoModeService (legacy) or facadeFactory (new).
*/
export function createStopHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createStopHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, branchName } = req.body as {
@@ -39,38 +31,6 @@ export function createStopHandler(
? `worktree ${normalizedBranchName}`
: 'main worktree';
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
// Check if running
if (!facade.isAutoLoopRunning(normalizedBranchName)) {
res.json({
success: true,
message: `Auto mode is not running for ${worktreeDesc}`,
wasRunning: false,
branchName: normalizedBranchName,
});
return;
}
// Stop the auto loop for this project/worktree
const runningCount = await facade.stopAutoLoop(normalizedBranchName);
logger.info(
`Stopped auto loop for ${worktreeDesc} in project: ${projectPath}, ${runningCount} features still running`
);
res.json({
success: true,
message: 'Auto mode stopped',
runningFeaturesCount: runningCount,
branchName: normalizedBranchName,
});
return;
}
// Legacy path: use autoModeService directly
// Check if running
if (!autoModeService.isAutoLoopRunningForProject(projectPath, normalizedBranchName)) {
res.json({

View File

@@ -3,18 +3,10 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create verify feature handler with transition compatibility.
* Accepts either autoModeService (legacy) or facadeFactory (new).
*/
export function createVerifyFeatureHandler(
autoModeService: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
) {
export function createVerifyFeatureHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId } = req.body as {
@@ -30,15 +22,6 @@ export function createVerifyFeatureHandler(
return;
}
// Use facade if factory is provided, otherwise fall back to autoModeService
if (facadeFactory) {
const facade = facadeFactory(projectPath);
const passes = await facade.verifyFeature(featureId);
res.json({ success: true, passes });
return;
}
// Legacy path: use autoModeService directly
const passes = await autoModeService.verifyFeature(projectPath, featureId);
res.json({ success: true, passes });
} catch (error) {

View File

@@ -5,8 +5,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 { AutoModeServiceCompat } 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';
@@ -25,15 +24,14 @@ export function createFeaturesRoutes(
featureLoader: FeatureLoader,
settingsService?: SettingsService,
events?: EventEmitter,
autoModeService?: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
autoModeService?: AutoModeServiceCompat
): Router {
const router = Router();
router.post(
'/list',
validatePathParams('projectPath'),
createListHandler(featureLoader, autoModeService, facadeFactory)
createListHandler(featureLoader, autoModeService)
);
router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader));
router.post(

View File

@@ -7,8 +7,7 @@
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 type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';
import { createLogger } from '@automaker/utils';
@@ -16,8 +15,7 @@ const logger = createLogger('FeaturesListRoute');
export function createListHandler(
featureLoader: FeatureLoader,
autoModeService?: AutoModeService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
autoModeService?: AutoModeServiceCompat
) {
return async (req: Request, res: Response): Promise<void> => {
try {
@@ -34,21 +32,7 @@ export function createListHandler(
// 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 (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) {
if (autoModeService) {
autoModeService.detectOrphanedFeatures(projectPath).then((orphanedFeatures) => {
if (orphanedFeatures.length > 0) {
logger.info(

View File

@@ -4,31 +4,23 @@
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 { AutoModeServiceCompat } 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';
export function createProjectsRoutes(
featureLoader: FeatureLoader,
autoModeService: AutoModeService,
autoModeService: AutoModeServiceCompat,
settingsService: SettingsService,
notificationService: NotificationService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
notificationService: NotificationService
): Router {
const router = Router();
// GET /overview - Get aggregate status for all projects
router.get(
'/overview',
createOverviewHandler(
featureLoader,
autoModeService,
settingsService,
notificationService,
facadeFactory
)
createOverviewHandler(featureLoader, autoModeService, settingsService, notificationService)
);
return router;

View File

@@ -9,9 +9,8 @@
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,
AutoModeServiceCompat,
RunningAgentInfo,
ProjectAutoModeStatus,
} from '../../../services/auto-mode/index.js';
@@ -152,10 +151,9 @@ function getLastActivityAt(features: Feature[]): string | undefined {
export function createOverviewHandler(
featureLoader: FeatureLoader,
autoModeService: AutoModeService,
autoModeService: AutoModeServiceCompat,
settingsService: SettingsService,
notificationService: NotificationService,
facadeFactory?: (projectPath: string) => AutoModeServiceFacade
notificationService: NotificationService
) {
return async (_req: Request, res: Response): Promise<void> => {
try {
@@ -164,15 +162,7 @@ export function createOverviewHandler(
const projectRefs: ProjectRef[] = settings.projects || [];
// Get all running agents once to count live running features per project
// 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();
}
const allRunningAgents: RunningAgentInfo[] = await autoModeService.getRunningAgents();
// Collect project statuses in parallel
const projectStatusPromises = projectRefs.map(async (projectRef): Promise<ProjectStatus> => {
@@ -183,13 +173,10 @@ export function createOverviewHandler(
const totalFeatures = features.length;
// Get auto-mode status for this project (main worktree, branchName = null)
let autoModeStatus: ProjectAutoModeStatus;
if (facadeFactory) {
const facade = facadeFactory(projectRef.path);
autoModeStatus = facade.getStatusForProject(null);
} else {
autoModeStatus = autoModeService.getStatusForProject(projectRef.path, null);
}
const autoModeStatus: ProjectAutoModeStatus = autoModeService.getStatusForProject(
projectRef.path,
null
);
const isAutoModeRunning = autoModeStatus.isAutoLoopRunning;
// Count live running features for this project (across all branches)

View File

@@ -3,10 +3,10 @@
*/
import { Router } from 'express';
import type { AutoModeService } from '../../services/auto-mode-service.js';
import type { AutoModeServiceCompat } from '../../services/auto-mode/index.js';
import { createIndexHandler } from './routes/index.js';
export function createRunningAgentsRoutes(autoModeService: AutoModeService): Router {
export function createRunningAgentsRoutes(autoModeService: AutoModeServiceCompat): Router {
const router = Router();
router.get('/', createIndexHandler(autoModeService));

View File

@@ -3,29 +3,16 @@
*/
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import type { AutoModeServiceFacade } from '../../../services/auto-mode/index.js';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getBacklogPlanStatus, getRunningDetails } from '../../backlog-plan/common.js';
import { getAllRunningGenerations } from '../../app-spec/common.js';
import path from 'path';
import { getErrorMessage, logError } from '../common.js';
/**
* Create index handler with transition compatibility.
* Accepts either autoModeService (legacy) or facade (new).
* Note: getRunningAgents is global (not per-project), so facade is created
* with an empty path for global queries.
*/
export function createIndexHandler(
autoModeService: AutoModeService,
facade?: AutoModeServiceFacade
) {
export function createIndexHandler(autoModeService: AutoModeServiceCompat) {
return async (_req: Request, res: Response): Promise<void> => {
try {
// Use facade if provided, otherwise fall back to autoModeService
const runningAgents = facade
? [...(await facade.getRunningAgents())]
: [...(await autoModeService.getRunningAgents())];
const runningAgents = [...(await autoModeService.getRunningAgents())];
const backlogPlanStatus = getBacklogPlanStatus();
const backlogPlanDetails = getRunningDetails();