Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions OPENCODE_CONFIG_CONTENT

This file was deleted.

3 changes: 3 additions & 0 deletions apps/server/null/STANDUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
2026-03-01T20:55:55.690Z

[object Object]
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file appears to have been accidentally committed. It contains [object Object], which often indicates an object was incorrectly stringified during development. This file should be removed from the pull request.

2 changes: 2 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"@github/copilot-sdk": "^0.1.16",
"@modelcontextprotocol/sdk": "1.25.2",
"@openai/codex-sdk": "^0.98.0",
"archiver": "^7.0.1",
"chokidar": "^4.0.3",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"dotenv": "17.2.3",
Expand Down
100 changes: 100 additions & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ import { createEventHistoryRoutes } from './routes/event-history/index.js';
import { getEventHistoryService } from './services/event-history-service.js';
import { getTestRunnerService } from './services/test-runner-service.js';
import { createProjectsRoutes } from './routes/projects/index.js';
import { createAutomationRoutes } from './routes/automation/index.js';
import {
initializeAutomationSchedulerService,
shutdownAutomationSchedulerService,
} from './services/automation-scheduler-service.js';
import { AutomationRuntimeEngine } from './services/automation-runtime-engine.js';
import { getAutomationVariableService } from './services/automation-variable-service.js';

// Load environment variables
dotenv.config();
Expand Down Expand Up @@ -370,6 +377,13 @@ testRunnerService.setEventEmitter(events);
// Initialize Event Hook Service for custom event triggers (with history storage)
eventHookService.initialize(events, settingsService, eventHistoryService, featureLoader);

// Initialize Automation Runtime Engine and Scheduler Service
// Pass settingsService so AI prompt steps can access credentials for Claude API authentication
const automationRuntimeEngine = AutomationRuntimeEngine.create(DATA_DIR, settingsService);
let automationSchedulerService: Awaited<
ReturnType<typeof initializeAutomationSchedulerService>
> | null = null;

// Initialize services
(async () => {
// Migrate settings from legacy Electron userData location if needed
Expand Down Expand Up @@ -461,6 +475,68 @@ eventHookService.initialize(events, settingsService, eventHistoryService, featur
void codexModelCacheService.getModels().catch((err) => {
logger.error('Failed to bootstrap Codex model cache:', err);
});

// Initialize Automation Scheduler Service
try {
automationSchedulerService = await initializeAutomationSchedulerService(
DATA_DIR,
events,
automationRuntimeEngine
);

// Set up auto mode operations for automation steps
automationSchedulerService.setAutoModeOperations({
start: async (projectPath, branchName, maxConcurrency) => {
const resolvedMaxConcurrency = await autoModeService.startAutoLoopForProject(
projectPath,
branchName ?? null,
maxConcurrency
);
return {
success: true,
maxConcurrency: resolvedMaxConcurrency,
message: `Auto mode started with max ${resolvedMaxConcurrency} concurrent features`,
};
},
stop: async (projectPath, branchName) => {
const runningCount = await autoModeService.stopAutoLoopForProject(
projectPath,
branchName ?? null
);
return {
success: true,
runningFeaturesCount: runningCount,
message: 'Auto mode stopped',
};
},
getStatus: async (projectPath, branchName) => {
const status = await autoModeService.getStatusForProject(projectPath, branchName ?? null);
return {
isRunning: status.runningCount > 0,
isAutoLoopRunning: status.isAutoLoopRunning,
runningFeatures: status.runningFeatures,
runningCount: status.runningCount,
maxConcurrency: status.maxConcurrency,
};
},
setConcurrency: async (projectPath, maxConcurrency, branchName) => {
// Start/restart auto mode with new concurrency
const resolvedMaxConcurrency = await autoModeService.startAutoLoopForProject(
projectPath,
branchName ?? null,
maxConcurrency
);
return {
success: true,
maxConcurrency: resolvedMaxConcurrency,
};
},
});

logger.info('Automation scheduler service initialized');
} catch (err) {
logger.error('Failed to initialize automation scheduler service:', err);
}
})();

// Run stale validation cleanup every hour to prevent memory leaks from crashed validations
Expand Down Expand Up @@ -522,6 +598,26 @@ app.use(
createProjectsRoutes(featureLoader, autoModeService, settingsService, notificationService)
);

// Automation routes (with null check for scheduler service)
app.use(
'/api/automation',
(req, res, next) => {
if (!automationSchedulerService) {
res.status(503).json({ success: false, error: 'Automation scheduler not initialized' });
return;
}
next();
},
(req, res, next) => {
const variableService = getAutomationVariableService();
createAutomationRoutes(automationSchedulerService!, automationRuntimeEngine, variableService)(
req,
res,
next
);
}
);

// Create HTTP server
const server = createServer(app);

Expand Down Expand Up @@ -840,6 +936,7 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
// Start server with error handling for port conflicts
const startServer = (port: number, host: string) => {
server.listen(port, host, () => {
logger.info('Gemini test - Hello World');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This appears to be a temporary debug log statement. It should be removed before merging to avoid cluttering production logs.

const terminalStatus = isTerminalEnabled()
? isTerminalPasswordRequired()
? 'enabled (password protected)'
Expand Down Expand Up @@ -962,6 +1059,9 @@ const gracefulShutdown = async (signal: string) => {
// Note: markAllRunningFeaturesInterrupted handles errors internally and never rejects
await autoModeService.markAllRunningFeaturesInterrupted(`${signal} signal received`);

// Shutdown automation scheduler service
await shutdownAutomationSchedulerService();

terminalService.cleanup();
server.close(() => {
clearTimeout(forceExitTimeout);
Expand Down
40 changes: 36 additions & 4 deletions apps/server/src/providers/claude-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ export class ClaudeProvider extends BaseProvider {
const maxThinkingTokens =
thinkingLevel === 'adaptive' ? undefined : getThinkingTokenBudget(thinkingLevel);

// Capture stderr output from the Claude Code subprocess for diagnostics.
// When the process exits with a non-zero code, stderr typically contains
// the actual error (auth failure, invalid model, etc.) that we need to
// surface to the user instead of the generic "process exited with code N".
const stderrChunks: string[] = [];

// Build Claude SDK options
const sdkOptions: Options = {
model,
Expand Down Expand Up @@ -249,6 +255,11 @@ export class ClaudeProvider extends BaseProvider {
...(options.agents && { agents: options.agents }),
// Pass through outputFormat for structured JSON outputs
...(options.outputFormat && { outputFormat: options.outputFormat }),
// Capture stderr for diagnostic information on process failures
stderr: (chunk: string) => {
stderrChunks.push(chunk);
logger.debug('[ClaudeProvider] stderr:', chunk.trimEnd());
},
};

// Build prompt payload
Expand Down Expand Up @@ -297,27 +308,48 @@ export class ClaudeProvider extends BaseProvider {
// Enhance error with user-friendly message and classification
const errorInfo = classifyError(error);
const userMessage = getUserFriendlyErrorMessage(error);
const stderrOutput = stderrChunks.join('').trim();

logger.error('executeQuery() error during execution:', {
type: errorInfo.type,
message: errorInfo.message,
isRateLimit: errorInfo.isRateLimit,
retryAfter: errorInfo.retryAfter,
stderr: stderrOutput || '(no stderr captured)',
stack: (error as Error).stack,
});

// Build enhanced error message with additional guidance for rate limits
const message = errorInfo.isRateLimit
? `${userMessage}\n\nTip: If you're running multiple features in auto-mode, consider reducing concurrency (maxConcurrency setting) to avoid hitting rate limits.`
: userMessage;
// When the process exits with a non-zero code and stderr has useful info,
// include it in the error message so upstream callers (e.g., automation
// engine) can surface the real cause to the user.
let message: string;
const rawMessage = error instanceof Error ? error.message : String(error);
const isProcessExit =
rawMessage.includes('Claude Code process exited') ||
rawMessage.includes('Claude Code process terminated');

if (isProcessExit && stderrOutput) {
// Extract the most useful part of stderr (last meaningful lines)
const stderrLines = stderrOutput.split('\n').filter(Boolean);
const relevantStderr = stderrLines.slice(-5).join('; ');
message = `${userMessage} (stderr: ${relevantStderr})`;
} else if (errorInfo.isRateLimit) {
message = `${userMessage}\n\nTip: If you're running multiple features in auto-mode, consider reducing concurrency (maxConcurrency setting) to avoid hitting rate limits.`;
} else {
message = userMessage;
}

const enhancedError = new Error(message) as Error & {
originalError: unknown;
type: string;
retryAfter?: number;
stderr?: string;
};
enhancedError.originalError = error;
enhancedError.type = errorInfo.type;
if (stderrOutput) {
enhancedError.stderr = stderrOutput;
}

if (errorInfo.isRateLimit) {
enhancedError.retryAfter = errorInfo.retryAfter;
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/providers/simple-query-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ export async function simpleQuery(options: SimpleQueryOptions): Promise<SimpleQu
break;
} else if (msg.subtype === 'error_max_structured_output_retries') {
throw new Error('Could not produce valid structured output after retries');
} else if (msg.subtype === 'error_during_execution') {
// SDK encountered an error during execution (API error, auth failure, etc.)
// The errors array contains the actual error messages from the CLI
const errors = (msg as unknown as Record<string, unknown>).errors as string[] | undefined;
const errorDetail = errors?.length ? errors.join('; ') : 'Unknown execution error';
throw new Error(`AI execution error: ${errorDetail}`);
} else if (msg.subtype === 'error_max_budget_usd') {
throw new Error('AI query exceeded the maximum budget limit');
}
}
}
Expand Down Expand Up @@ -265,6 +273,13 @@ export async function streamingQuery(options: StreamingQueryOptions): Promise<Si
break;
} else if (msg.subtype === 'error_max_structured_output_retries') {
throw new Error('Could not produce valid structured output after retries');
} else if (msg.subtype === 'error_during_execution') {
// SDK encountered an error during execution (API error, auth failure, etc.)
const errors = (msg as unknown as Record<string, unknown>).errors as string[] | undefined;
const errorDetail = errors?.length ? errors.join('; ') : 'Unknown execution error';
throw new Error(`AI execution error: ${errorDetail}`);
} else if (msg.subtype === 'error_max_budget_usd') {
throw new Error('AI query exceeded the maximum budget limit');
}
}
}
Expand Down
25 changes: 10 additions & 15 deletions apps/server/src/routes/auto-mode/routes/reconcile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,22 @@
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';

const logger = createLogger('ReconcileFeatures');

interface ReconcileRequest {
projectPath: string;
}

export function createReconcileHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
const { projectPath } = req.body as ReconcileRequest;
try {
const { projectPath } = req.body as { projectPath: string };

if (!projectPath) {
res.status(400).json({ error: 'Project path is required' });
return;
}
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}

logger.info(`Reconciling feature states for ${projectPath}`);
logger.info(`Reconciling feature states for ${projectPath}`);

try {
const reconciledCount = await autoModeService.reconcileFeatureStates(projectPath);

res.json({
Expand All @@ -44,10 +41,8 @@ export function createReconcileHandler(autoModeService: AutoModeServiceCompat) {
: 'No features needed reconciliation',
});
} catch (error) {
logger.error('Error reconciling feature states:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
logError(error, 'Reconcile feature states failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}
25 changes: 10 additions & 15 deletions apps/server/src/routes/auto-mode/routes/resume-interrupted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,31 @@
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import type { AutoModeServiceCompat } from '../../../services/auto-mode/index.js';
import { getErrorMessage, logError } from '../common.js';

const logger = createLogger('ResumeInterrupted');

interface ResumeInterruptedRequest {
projectPath: string;
}

export function createResumeInterruptedHandler(autoModeService: AutoModeServiceCompat) {
return async (req: Request, res: Response): Promise<void> => {
const { projectPath } = req.body as ResumeInterruptedRequest;
try {
const { projectPath } = req.body as { projectPath: string };

if (!projectPath) {
res.status(400).json({ error: 'Project path is required' });
return;
}
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}

logger.info(`Checking for interrupted features in ${projectPath}`);
logger.info(`Checking for interrupted features in ${projectPath}`);

try {
await autoModeService.resumeInterruptedFeatures(projectPath);

res.json({
success: true,
message: 'Resume check completed',
});
} catch (error) {
logger.error('Error resuming interrupted features:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
logError(error, 'Resume interrupted features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}
13 changes: 13 additions & 0 deletions apps/server/src/routes/auto-mode/routes/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export function createStartHandler(autoModeService: AutoModeServiceCompat) {
return;
}

if (
maxConcurrency !== undefined &&
(typeof maxConcurrency !== 'number' ||
maxConcurrency < 1 ||
!Number.isFinite(maxConcurrency))
) {
res.status(400).json({
success: false,
error: 'maxConcurrency must be a positive integer',
});
return;
}

// Normalize branchName: undefined becomes null
const normalizedBranchName = branchName ?? null;
const worktreeDesc = normalizedBranchName
Expand Down
Loading
Loading