feat: implement structured logging across server components

- Integrated a centralized logging system using createLogger from @automaker/utils, replacing console.log and console.error statements with logger methods for consistent log formatting and improved readability.
- Updated various modules, including auth, events, and services, to utilize the new logging system, enhancing error tracking and operational visibility.
- Refactored logging messages to provide clearer context and information, ensuring better maintainability and debugging capabilities.

This update significantly enhances the observability of the server components, facilitating easier troubleshooting and monitoring.
This commit is contained in:
Shirone
2026-01-02 15:40:15 +01:00
parent 8c04e0028f
commit 96a999817f
23 changed files with 284 additions and 275 deletions

View File

@@ -25,7 +25,10 @@ import {
isAbortError,
classifyError,
loadContextFiles,
createLogger,
} from '@automaker/utils';
const logger = createLogger('AutoMode');
import { resolveModelString, resolvePhaseModel, DEFAULT_MODELS } from '@automaker/model-resolver';
import { resolveDependencies, areDependenciesSatisfied } from '@automaker/dependency-resolver';
import { getFeatureDir, getAutomakerDir, getFeaturesDir } from '@automaker/platform';
@@ -262,8 +265,8 @@ export class AutoModeService {
this.pausedDueToFailures = true;
const failureCount = this.consecutiveFailures.length;
console.log(
`[AutoMode] Pausing auto loop after ${failureCount} consecutive failures. Last error: ${errorInfo.type}`
logger.info(
`Pausing auto loop after ${failureCount} consecutive failures. Last error: ${errorInfo.type}`
);
// Emit event to notify UI
@@ -323,7 +326,7 @@ export class AutoModeService {
// Run the loop in the background
this.runAutoLoop().catch((error) => {
console.error('[AutoMode] Loop error:', error);
logger.error('Loop error:', error);
const errorInfo = classifyError(error);
this.emitAutoModeEvent('auto_mode_error', {
error: errorInfo.message,
@@ -368,13 +371,13 @@ export class AutoModeService {
this.config!.useWorktrees,
true
).catch((error) => {
console.error(`[AutoMode] Feature ${nextFeature.id} error:`, error);
logger.error(`Feature ${nextFeature.id} error:`, error);
});
}
await this.sleep(2000);
} catch (error) {
console.error('[AutoMode] Loop iteration error:', error);
logger.error('Loop iteration error:', error);
await this.sleep(5000);
}
}
@@ -447,8 +450,8 @@ export class AutoModeService {
if (!options?.continuationPrompt) {
const hasExistingContext = await this.contextExists(projectPath, featureId);
if (hasExistingContext) {
console.log(
`[AutoMode] Feature ${featureId} has existing context, resuming instead of starting fresh`
logger.info(
`Feature ${featureId} has existing context, resuming instead of starting fresh`
);
// Remove from running features temporarily, resumeFeature will add it back
this.runningFeatures.delete(featureId);
@@ -483,12 +486,10 @@ export class AutoModeService {
worktreePath = await this.findExistingWorktreeForBranch(projectPath, branchName);
if (worktreePath) {
console.log(`[AutoMode] Using worktree for branch "${branchName}": ${worktreePath}`);
logger.info(`Using worktree for branch "${branchName}": ${worktreePath}`);
} else {
// Worktree doesn't exist - log warning and continue with project path
console.warn(
`[AutoMode] Worktree for branch "${branchName}" not found, using project path`
);
logger.warn(`Worktree for branch "${branchName}" not found, using project path`);
}
}
@@ -528,7 +529,7 @@ export class AutoModeService {
// Continuation prompt is used when recovering from a plan approval
// The plan was already approved, so skip the planning phase
prompt = options.continuationPrompt;
console.log(`[AutoMode] Using continuation prompt for feature ${featureId}`);
logger.info(`Using continuation prompt for feature ${featureId}`);
} else {
// Normal flow: build prompt with planning phase
const featurePrompt = this.buildFeaturePrompt(feature);
@@ -553,8 +554,8 @@ export class AutoModeService {
// Get model from feature and determine provider
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
const provider = ProviderFactory.getProviderNameForModel(model);
console.log(
`[AutoMode] Executing feature ${featureId} with model: ${model}, provider: ${provider} in ${workDir}`
logger.info(
`Executing feature ${featureId} with model: ${model}, provider: ${provider} in ${workDir}`
);
// Store model and provider in running feature for tracking
@@ -628,7 +629,7 @@ export class AutoModeService {
projectPath,
});
} else {
console.error(`[AutoMode] Feature ${featureId} failed:`, error);
logger.error(`Feature ${featureId} failed:`, error);
await this.updateFeatureStatus(projectPath, featureId, 'backlog');
this.emitAutoModeEvent('auto_mode_error', {
featureId,
@@ -653,9 +654,9 @@ export class AutoModeService {
}
}
} finally {
console.log(`[AutoMode] Feature ${featureId} execution ended, cleaning up runningFeatures`);
console.log(
`[AutoMode] Pending approvals at cleanup: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
logger.info(`Feature ${featureId} execution ended, cleaning up runningFeatures`);
logger.info(
`Pending approvals at cleanup: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
);
this.runningFeatures.delete(featureId);
}
@@ -673,7 +674,7 @@ export class AutoModeService {
abortController: AbortController,
autoLoadClaudeMd: boolean
): Promise<void> {
console.log(`[AutoMode] Executing ${steps.length} pipeline step(s) for feature ${featureId}`);
logger.info(`Executing ${steps.length} pipeline step(s) for feature ${featureId}`);
// Load context files once
const contextResult = await loadContextFiles({
@@ -756,12 +757,12 @@ export class AutoModeService {
projectPath,
});
console.log(
`[AutoMode] Pipeline step ${i + 1}/${steps.length} (${step.name}) completed for feature ${featureId}`
logger.info(
`Pipeline step ${i + 1}/${steps.length} (${step.name}) completed for feature ${featureId}`
);
}
console.log(`[AutoMode] All pipeline steps completed for feature ${featureId}`);
logger.info(`All pipeline steps completed for feature ${featureId}`);
}
/**
@@ -886,7 +887,7 @@ Complete the pipeline step instructions above. Review the previous work and appl
if (worktreePath) {
workDir = worktreePath;
console.log(`[AutoMode] Follow-up using worktree for branch "${branchName}": ${workDir}`);
logger.info(`Follow-up using worktree for branch "${branchName}": ${workDir}`);
}
}
@@ -942,9 +943,7 @@ Address the follow-up instructions above. Review the previous work and make the
// Get model from feature and determine provider early for tracking
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
const provider = ProviderFactory.getProviderNameForModel(model);
console.log(
`[AutoMode] Follow-up for feature ${featureId} using model: ${model}, provider: ${provider}`
);
logger.info(`Follow-up for feature ${featureId} using model: ${model}, provider: ${provider}`);
this.runningFeatures.set(featureId, {
featureId,
@@ -994,7 +993,7 @@ Address the follow-up instructions above. Review the previous work and make the
// Store the absolute path (external storage uses absolute paths)
copiedImagePaths.push(destPath);
} catch (error) {
console.error(`[AutoMode] Failed to copy follow-up image ${imagePath}:`, error);
logger.error(`Failed to copy follow-up image ${imagePath}:`, error);
}
}
}
@@ -1030,7 +1029,7 @@ Address the follow-up instructions above. Review the previous work and make the
try {
await secureFs.writeFile(featurePath, JSON.stringify(feature, null, 2));
} catch (error) {
console.error(`[AutoMode] Failed to save feature.json:`, error);
logger.error(`Failed to save feature.json:`, error);
}
}
@@ -1178,10 +1177,10 @@ Address the follow-up instructions above. Review the previous work and make the
try {
await secureFs.access(providedWorktreePath);
workDir = providedWorktreePath;
console.log(`[AutoMode] Committing in provided worktree: ${workDir}`);
logger.info(`Committing in provided worktree: ${workDir}`);
} catch {
console.log(
`[AutoMode] Provided worktree path doesn't exist: ${providedWorktreePath}, using project path`
logger.info(
`Provided worktree path doesn't exist: ${providedWorktreePath}, using project path`
);
}
} else {
@@ -1190,9 +1189,9 @@ Address the follow-up instructions above. Review the previous work and make the
try {
await secureFs.access(legacyWorktreePath);
workDir = legacyWorktreePath;
console.log(`[AutoMode] Committing in legacy worktree: ${workDir}`);
logger.info(`Committing in legacy worktree: ${workDir}`);
} catch {
console.log(`[AutoMode] No worktree found, committing in project path: ${workDir}`);
logger.info(`No worktree found, committing in project path: ${workDir}`);
}
}
@@ -1232,7 +1231,7 @@ Address the follow-up instructions above. Review the previous work and make the
return hash.trim();
} catch (error) {
console.error(`[AutoMode] Commit failed for ${featureId}:`, error);
logger.error(`Commit failed for ${featureId}:`, error);
return null;
}
}
@@ -1286,7 +1285,7 @@ Format your response as a structured markdown document.`;
settings?.phaseModels?.projectAnalysisModel || DEFAULT_PHASE_MODELS.projectAnalysisModel;
const { model: analysisModel, thinkingLevel: analysisThinkingLevel } =
resolvePhaseModel(phaseModelEntry);
console.log('[AutoMode] Using model for project analysis:', analysisModel);
logger.info('Using model for project analysis:', analysisModel);
const provider = ProviderFactory.getProviderForModel(analysisModel);
@@ -1432,9 +1431,9 @@ Format your response as a structured markdown document.`;
featureId: string,
projectPath: string
): Promise<{ approved: boolean; editedPlan?: string; feedback?: string }> {
console.log(`[AutoMode] Registering pending approval for feature ${featureId}`);
console.log(
`[AutoMode] Current pending approvals: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
logger.info(`Registering pending approval for feature ${featureId}`);
logger.info(
`Current pending approvals: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
);
return new Promise((resolve, reject) => {
this.pendingApprovals.set(featureId, {
@@ -1443,7 +1442,7 @@ Format your response as a structured markdown document.`;
featureId,
projectPath,
});
console.log(`[AutoMode] Pending approval registered for feature ${featureId}`);
logger.info(`Pending approval registered for feature ${featureId}`);
});
}
@@ -1458,27 +1457,23 @@ Format your response as a structured markdown document.`;
feedback?: string,
projectPathFromClient?: string
): Promise<{ success: boolean; error?: string }> {
console.log(
`[AutoMode] resolvePlanApproval called for feature ${featureId}, approved=${approved}`
);
console.log(
`[AutoMode] Current pending approvals: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
logger.info(`resolvePlanApproval called for feature ${featureId}, approved=${approved}`);
logger.info(
`Current pending approvals: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
);
const pending = this.pendingApprovals.get(featureId);
if (!pending) {
console.log(`[AutoMode] No pending approval in Map for feature ${featureId}`);
logger.info(`No pending approval in Map for feature ${featureId}`);
// RECOVERY: If no pending approval but we have projectPath from client,
// check if feature's planSpec.status is 'generated' and handle recovery
if (projectPathFromClient) {
console.log(`[AutoMode] Attempting recovery with projectPath: ${projectPathFromClient}`);
logger.info(`Attempting recovery with projectPath: ${projectPathFromClient}`);
const feature = await this.loadFeature(projectPathFromClient, featureId);
if (feature?.planSpec?.status === 'generated') {
console.log(
`[AutoMode] Feature ${featureId} has planSpec.status='generated', performing recovery`
);
logger.info(`Feature ${featureId} has planSpec.status='generated', performing recovery`);
if (approved) {
// Update planSpec to approved
@@ -1497,17 +1492,14 @@ Format your response as a structured markdown document.`;
}
continuationPrompt += `Now proceed with the implementation as specified in the plan:\n\n${planContent}\n\nImplement the feature now.`;
console.log(`[AutoMode] Starting recovery execution for feature ${featureId}`);
logger.info(`Starting recovery execution for feature ${featureId}`);
// Start feature execution with the continuation prompt (async, don't await)
// Pass undefined for providedWorktreePath, use options for continuation prompt
this.executeFeature(projectPathFromClient, featureId, true, false, undefined, {
continuationPrompt,
}).catch((error) => {
console.error(
`[AutoMode] Recovery execution failed for feature ${featureId}:`,
error
);
logger.error(`Recovery execution failed for feature ${featureId}:`, error);
});
return { success: true };
@@ -1531,15 +1523,15 @@ Format your response as a structured markdown document.`;
}
}
console.log(
`[AutoMode] ERROR: No pending approval found for feature ${featureId} and recovery not possible`
logger.info(
`ERROR: No pending approval found for feature ${featureId} and recovery not possible`
);
return {
success: false,
error: `No pending approval for feature ${featureId}`,
};
}
console.log(`[AutoMode] Found pending approval for feature ${featureId}, proceeding...`);
logger.info(`Found pending approval for feature ${featureId}, proceeding...`);
const { projectPath } = pending;
@@ -1572,17 +1564,17 @@ Format your response as a structured markdown document.`;
* Cancel a pending plan approval (e.g., when feature is stopped).
*/
cancelPlanApproval(featureId: string): void {
console.log(`[AutoMode] cancelPlanApproval called for feature ${featureId}`);
console.log(
`[AutoMode] Current pending approvals: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
logger.info(`cancelPlanApproval called for feature ${featureId}`);
logger.info(
`Current pending approvals: ${Array.from(this.pendingApprovals.keys()).join(', ') || 'none'}`
);
const pending = this.pendingApprovals.get(featureId);
if (pending) {
console.log(`[AutoMode] Found and cancelling pending approval for feature ${featureId}`);
logger.info(`Found and cancelling pending approval for feature ${featureId}`);
pending.reject(new Error('Plan approval cancelled - feature was stopped'));
this.pendingApprovals.delete(featureId);
} else {
console.log(`[AutoMode] No pending approval to cancel for feature ${featureId}`);
logger.info(`No pending approval to cancel for feature ${featureId}`);
}
}
@@ -1722,7 +1714,7 @@ Format your response as a structured markdown document.`;
feature.updatedAt = new Date().toISOString();
await secureFs.writeFile(featurePath, JSON.stringify(feature, null, 2));
} catch (error) {
console.error(`[AutoMode] Failed to update planSpec for ${featureId}:`, error);
logger.error(`Failed to update planSpec for ${featureId}:`, error);
}
}
@@ -1980,7 +1972,7 @@ This helps parse your summary correctly in the output logs.`;
// CI/CD Mock Mode: Return early with mock response when AUTOMAKER_MOCK_AGENT is set
// This prevents actual API calls during automated testing
if (process.env.AUTOMAKER_MOCK_AGENT === 'true') {
console.log(`[AutoMode] MOCK MODE: Skipping real agent execution for feature ${featureId}`);
logger.info(`MOCK MODE: Skipping real agent execution for feature ${featureId}`);
// Simulate some work being done
await this.sleep(500);
@@ -2030,7 +2022,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
await secureFs.mkdir(path.dirname(outputPath), { recursive: true });
await secureFs.writeFile(outputPath, mockOutput);
console.log(`[AutoMode] MOCK MODE: Completed mock execution for feature ${featureId}`);
logger.info(`MOCK MODE: Completed mock execution for feature ${featureId}`);
return;
}
@@ -2068,14 +2060,14 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
const maxTurns = sdkOptions.maxTurns;
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
console.log(
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}, planningMode: ${planningMode}, requiresApproval: ${requiresApproval}`
logger.info(
`runAgent called for feature ${featureId} with model: ${finalModel}, planningMode: ${planningMode}, requiresApproval: ${requiresApproval}`
);
// Get provider for this model
const provider = ProviderFactory.getProviderForModel(finalModel);
console.log(`[AutoMode] Using provider "${provider.getName()}" for model "${finalModel}"`);
logger.info(`Using provider "${provider.getName()}" for model "${finalModel}"`);
// Build prompt content with images using utility
const { content: promptContent } = await buildPromptWithImages(
@@ -2087,8 +2079,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
// Debug: Log if system prompt is provided
if (options?.systemPrompt) {
console.log(
`[AutoMode] System prompt provided (${options.systemPrompt.length} chars), first 200 chars:\n${options.systemPrompt.substring(0, 200)}...`
logger.info(
`System prompt provided (${options.systemPrompt.length} chars), first 200 chars:\n${options.systemPrompt.substring(0, 200)}...`
);
}
@@ -2109,9 +2101,9 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
};
// Execute via provider
console.log(`[AutoMode] Starting stream for feature ${featureId}...`);
logger.info(`Starting stream for feature ${featureId}...`);
const stream = provider.executeQuery(executeOptions);
console.log(`[AutoMode] Stream created, starting to iterate...`);
logger.info(`Stream created, starting to iterate...`);
// Initialize with previous content if this is a follow-up, with a separator
let responseText = previousContent
? `${previousContent}\n\n---\n\n## Follow-up Session\n\n`
@@ -2157,7 +2149,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
await secureFs.appendFile(rawOutputPath, rawOutputLines.join('\n') + '\n');
rawOutputLines = []; // Clear after writing
} catch (error) {
console.error(`[AutoMode] Failed to write raw output for ${featureId}:`, error);
logger.error(`Failed to write raw output for ${featureId}:`, error);
}
}, WRITE_DEBOUNCE_MS);
} catch {
@@ -2172,7 +2164,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
await secureFs.writeFile(outputPath, responseText);
} catch (error) {
// Log but don't crash - file write errors shouldn't stop execution
console.error(`[AutoMode] Failed to write agent output for ${featureId}:`, error);
logger.error(`Failed to write agent output for ${featureId}:`, error);
}
};
@@ -2190,7 +2182,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
// Log raw stream event for debugging
appendRawEvent(msg);
console.log(`[AutoMode] Stream message received:`, msg.type, msg.subtype || '');
logger.info(`Stream message received:`, msg.type, msg.subtype || '');
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
@@ -2255,11 +2247,9 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
let parsedTasks = parseTasksFromSpec(planContent);
const tasksTotal = parsedTasks.length;
console.log(
`[AutoMode] Parsed ${tasksTotal} tasks from spec for feature ${featureId}`
);
logger.info(`Parsed ${tasksTotal} tasks from spec for feature ${featureId}`);
if (parsedTasks.length > 0) {
console.log(`[AutoMode] Tasks: ${parsedTasks.map((t) => t.id).join(', ')}`);
logger.info(`Tasks: ${parsedTasks.map((t) => t.id).join(', ')}`);
}
// Update planSpec status to 'generated' and save content with parsed tasks
@@ -2288,8 +2278,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
let planApproved = false;
while (!planApproved) {
console.log(
`[AutoMode] Spec v${planVersion} generated for feature ${featureId}, waiting for approval`
logger.info(
`Spec v${planVersion} generated for feature ${featureId}, waiting for approval`
);
// CRITICAL: Register pending approval BEFORE emitting event
@@ -2310,9 +2300,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
if (approvalResult.approved) {
// User approved the plan
console.log(
`[AutoMode] Plan v${planVersion} approved for feature ${featureId}`
);
logger.info(`Plan v${planVersion} approved for feature ${featureId}`);
planApproved = true;
// If user provided edits, use the edited version
@@ -2344,15 +2332,15 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
if (!hasFeedback && !hasEdits) {
// No feedback or edits = explicit cancel
console.log(
`[AutoMode] Plan rejected without feedback for feature ${featureId}, cancelling`
logger.info(
`Plan rejected without feedback for feature ${featureId}, cancelling`
);
throw new Error('Plan cancelled by user');
}
// User wants revisions - regenerate the plan
console.log(
`[AutoMode] Plan v${planVersion} rejected with feedback for feature ${featureId}, regenerating...`
logger.info(
`Plan v${planVersion} rejected with feedback for feature ${featureId}, regenerating...`
);
planVersion++;
@@ -2429,7 +2417,7 @@ After generating the revised spec, output:
// Re-parse tasks from revised plan
const revisedTasks = parseTasksFromSpec(currentPlanContent);
console.log(`[AutoMode] Revised plan has ${revisedTasks.length} tasks`);
logger.info(`Revised plan has ${revisedTasks.length} tasks`);
// Update planSpec with revised content
await this.updateFeaturePlanSpec(projectPath, featureId, {
@@ -2455,8 +2443,8 @@ After generating the revised spec, output:
}
} else {
// Auto-approve: requirePlanApproval is false, just continue without pausing
console.log(
`[AutoMode] Spec generated for feature ${featureId}, auto-approving (requirePlanApproval=false)`
logger.info(
`Spec generated for feature ${featureId}, auto-approving (requirePlanApproval=false)`
);
// Emit info event for frontend
@@ -2472,9 +2460,7 @@ After generating the revised spec, output:
// CRITICAL: After approval, we need to make a second call to continue implementation
// The agent is waiting for "approved" - we need to send it and continue
console.log(
`[AutoMode] Making continuation call after plan approval for feature ${featureId}`
);
logger.info(`Making continuation call after plan approval for feature ${featureId}`);
// Update planSpec status to approved (handles both manual and auto-approval paths)
await this.updateFeaturePlanSpec(projectPath, featureId, {
@@ -2489,8 +2475,8 @@ After generating the revised spec, output:
// ========================================
if (parsedTasks.length > 0) {
console.log(
`[AutoMode] Starting multi-agent execution: ${parsedTasks.length} tasks for feature ${featureId}`
logger.info(
`Starting multi-agent execution: ${parsedTasks.length} tasks for feature ${featureId}`
);
// Execute each task with a separate agent
@@ -2503,7 +2489,7 @@ After generating the revised spec, output:
}
// Emit task started
console.log(`[AutoMode] Starting task ${task.id}: ${task.description}`);
logger.info(`Starting task ${task.id}: ${task.description}`);
this.emitAutoModeEvent('auto_mode_task_started', {
featureId,
projectPath,
@@ -2570,7 +2556,7 @@ After generating the revised spec, output:
}
// Emit task completed
console.log(`[AutoMode] Task ${task.id} completed for feature ${featureId}`);
logger.info(`Task ${task.id} completed for feature ${featureId}`);
this.emitAutoModeEvent('auto_mode_task_complete', {
featureId,
projectPath,
@@ -2601,13 +2587,11 @@ After generating the revised spec, output:
}
}
console.log(
`[AutoMode] All ${parsedTasks.length} tasks completed for feature ${featureId}`
);
logger.info(`All ${parsedTasks.length} tasks completed for feature ${featureId}`);
} else {
// No parsed tasks - fall back to single-agent execution
console.log(
`[AutoMode] No parsed tasks, using single-agent execution for feature ${featureId}`
logger.info(
`No parsed tasks, using single-agent execution for feature ${featureId}`
);
const continuationPrompt = `The plan/specification has been approved. Now implement it.
@@ -2657,15 +2641,15 @@ Implement all the changes described in the plan above.`;
}
}
console.log(`[AutoMode] Implementation completed for feature ${featureId}`);
logger.info(`Implementation completed for feature ${featureId}`);
// Exit the original stream loop since continuation is done
break streamLoop;
}
// Only emit progress for non-marker text (marker was already handled above)
if (!specDetected) {
console.log(
`[AutoMode] Emitting progress event for ${featureId}, content length: ${block.text?.length || 0}`
logger.info(
`Emitting progress event for ${featureId}, content length: ${block.text?.length || 0}`
);
this.emitAutoModeEvent('auto_mode_progress', {
featureId,
@@ -2719,7 +2703,7 @@ Implement all the changes described in the plan above.`;
await secureFs.mkdir(path.dirname(rawOutputPath), { recursive: true });
await secureFs.appendFile(rawOutputPath, rawOutputLines.join('\n') + '\n');
} catch (error) {
console.error(`[AutoMode] Failed to write final raw output for ${featureId}:`, error);
logger.error(`Failed to write final raw output for ${featureId}:`, error);
}
}
}

View File

@@ -11,6 +11,9 @@ import { spawn, execSync, type ChildProcess } from 'child_process';
import * as secureFs from '../lib/secure-fs.js';
import path from 'path';
import net from 'net';
import { createLogger } from '@automaker/utils';
const logger = createLogger('DevServerService');
export interface DevServerInfo {
worktreePath: string;
@@ -69,7 +72,7 @@ class DevServerService {
for (const pid of pids) {
try {
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
console.log(`[DevServerService] Killed process ${pid} on port ${port}`);
logger.debug(`Killed process ${pid} on port ${port}`);
} catch {
// Process may have already exited
}
@@ -82,7 +85,7 @@ class DevServerService {
for (const pid of pids) {
try {
execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
console.log(`[DevServerService] Killed process ${pid} on port ${port}`);
logger.debug(`Killed process ${pid} on port ${port}`);
} catch {
// Process may have already exited
}
@@ -93,7 +96,7 @@ class DevServerService {
}
} catch (error) {
// Ignore errors - port might not have any process
console.log(`[DevServerService] No process to kill on port ${port}`);
logger.debug(`No process to kill on port ${port}`);
}
}
@@ -251,11 +254,9 @@ class DevServerService {
// Small delay to ensure related ports are freed
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(`[DevServerService] Starting dev server on port ${port}`);
console.log(`[DevServerService] Working directory (cwd): ${worktreePath}`);
console.log(
`[DevServerService] Command: ${devCommand.cmd} ${devCommand.args.join(' ')} with PORT=${port}`
);
logger.info(`Starting dev server on port ${port}`);
logger.debug(`Working directory (cwd): ${worktreePath}`);
logger.debug(`Command: ${devCommand.cmd} ${devCommand.args.join(' ')} with PORT=${port}`);
// Spawn the dev process with PORT environment variable
const env = {
@@ -276,26 +277,26 @@ class DevServerService {
// Log output for debugging
if (devProcess.stdout) {
devProcess.stdout.on('data', (data: Buffer) => {
console.log(`[DevServer:${port}] ${data.toString().trim()}`);
logger.debug(`[Port${port}] ${data.toString().trim()}`);
});
}
if (devProcess.stderr) {
devProcess.stderr.on('data', (data: Buffer) => {
const msg = data.toString().trim();
console.error(`[DevServer:${port}] ${msg}`);
logger.debug(`[Port${port}] ${msg}`);
});
}
devProcess.on('error', (error) => {
console.error(`[DevServerService] Process error:`, error);
logger.error(`Process error:`, error);
status.error = error.message;
this.allocatedPorts.delete(port);
this.runningServers.delete(worktreePath);
});
devProcess.on('exit', (code) => {
console.log(`[DevServerService] Process for ${worktreePath} exited with code ${code}`);
logger.info(`Process for ${worktreePath} exited with code ${code}`);
status.exited = true;
this.allocatedPorts.delete(port);
this.runningServers.delete(worktreePath);
@@ -352,9 +353,7 @@ class DevServerService {
// If we don't have a record of this server, it may have crashed/exited on its own
// Return success so the frontend can clear its state
if (!server) {
console.log(
`[DevServerService] No server record for ${worktreePath}, may have already stopped`
);
logger.debug(`No server record for ${worktreePath}, may have already stopped`);
return {
success: true,
result: {
@@ -364,7 +363,7 @@ class DevServerService {
};
}
console.log(`[DevServerService] Stopping dev server for ${worktreePath}`);
logger.info(`Stopping dev server for ${worktreePath}`);
// Kill the process
if (server.process && !server.process.killed) {
@@ -434,7 +433,7 @@ class DevServerService {
* Stop all running dev servers (for cleanup)
*/
async stopAll(): Promise<void> {
console.log(`[DevServerService] Stopping all ${this.runningServers.size} dev servers`);
logger.info(`Stopping all ${this.runningServers.size} dev servers`);
for (const [worktreePath] of this.runningServers) {
await this.stopDevServer(worktreePath);

View File

@@ -56,10 +56,10 @@ export class FeatureLoader {
try {
// Paths are now absolute
await secureFs.unlink(oldPath);
console.log(`[FeatureLoader] Deleted orphaned image: ${oldPath}`);
logger.info(`Deleted orphaned image: ${oldPath}`);
} catch (error) {
// Ignore errors when deleting (file may already be gone)
logger.warn(`[FeatureLoader] Failed to delete image: ${oldPath}`, error);
logger.warn(`Failed to delete image: ${oldPath}`, error);
}
}
}
@@ -101,7 +101,7 @@ export class FeatureLoader {
try {
await secureFs.access(fullOriginalPath);
} catch {
logger.warn(`[FeatureLoader] Image not found, skipping: ${fullOriginalPath}`);
logger.warn(`Image not found, skipping: ${fullOriginalPath}`);
continue;
}
@@ -111,7 +111,7 @@ export class FeatureLoader {
// Copy the file
await secureFs.copyFile(fullOriginalPath, newPath);
console.log(`[FeatureLoader] Copied image: ${originalPath} -> ${newPath}`);
logger.info(`Copied image: ${originalPath} -> ${newPath}`);
// Try to delete the original temp file
try {
@@ -202,9 +202,7 @@ export class FeatureLoader {
const feature = JSON.parse(content);
if (!feature.id) {
logger.warn(
`[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping`
);
logger.warn(`Feature ${featureId} missing required 'id' field, skipping`);
return null;
}
@@ -213,14 +211,9 @@ export class FeatureLoader {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
} else if (error instanceof SyntaxError) {
logger.warn(
`[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}`
);
logger.warn(`Failed to parse feature.json for ${featureId}: ${error.message}`);
} else {
logger.error(
`[FeatureLoader] Failed to load feature ${featureId}:`,
(error as Error).message
);
logger.error(`Failed to load feature ${featureId}:`, (error as Error).message);
}
return null;
}
@@ -255,7 +248,7 @@ export class FeatureLoader {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.error(`[FeatureLoader] Failed to get feature ${featureId}:`, error);
logger.error(`Failed to get feature ${featureId}:`, error);
throw error;
}
}
@@ -342,10 +335,10 @@ export class FeatureLoader {
try {
const featureDir = this.getFeatureDir(projectPath, featureId);
await secureFs.rm(featureDir, { recursive: true, force: true });
console.log(`[FeatureLoader] Deleted feature ${featureId}`);
logger.info(`Deleted feature ${featureId}`);
return true;
} catch (error) {
logger.error(`[FeatureLoader] Failed to delete feature ${featureId}:`, error);
logger.error(`Failed to delete feature ${featureId}:`, error);
return false;
}
}
@@ -362,7 +355,7 @@ export class FeatureLoader {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.error(`[FeatureLoader] Failed to get agent output for ${featureId}:`, error);
logger.error(`Failed to get agent output for ${featureId}:`, error);
throw error;
}
}
@@ -379,7 +372,7 @@ export class FeatureLoader {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.error(`[FeatureLoader] Failed to get raw output for ${featureId}:`, error);
logger.error(`Failed to get raw output for ${featureId}:`, error);
throw error;
}
}

View File

@@ -12,6 +12,9 @@ import * as path from 'path';
// secureFs is used for user-controllable paths (working directory validation)
// to enforce ALLOWED_ROOT_DIRECTORY security boundary
import * as secureFs from '../lib/secure-fs.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger('Terminal');
// System paths module handles shell binary checks and WSL detection
// These are system paths outside ALLOWED_ROOT_DIRECTORY, centralized for security auditing
import {
@@ -219,7 +222,7 @@ export class TerminalService extends EventEmitter {
// Reject paths with null bytes (could bypass path checks)
if (cwd.includes('\0')) {
console.warn(`[Terminal] Rejecting path with null byte: ${cwd.replace(/\0/g, '\\0')}`);
logger.warn(`Rejecting path with null byte: ${cwd.replace(/\0/g, '\\0')}`);
return homeDir;
}
@@ -242,12 +245,10 @@ export class TerminalService extends EventEmitter {
if (statResult.isDirectory()) {
return cwd;
}
console.warn(`[Terminal] Path exists but is not a directory: ${cwd}, falling back to home`);
logger.warn(`Path exists but is not a directory: ${cwd}, falling back to home`);
return homeDir;
} catch {
console.warn(
`[Terminal] Working directory does not exist or not allowed: ${cwd}, falling back to home`
);
logger.warn(`Working directory does not exist or not allowed: ${cwd}, falling back to home`);
return homeDir;
}
}
@@ -272,7 +273,7 @@ export class TerminalService extends EventEmitter {
setMaxSessions(limit: number): void {
if (limit >= MIN_MAX_SESSIONS && limit <= MAX_MAX_SESSIONS) {
maxSessions = limit;
console.log(`[Terminal] Max sessions limit updated to ${limit}`);
logger.info(`Max sessions limit updated to ${limit}`);
}
}
@@ -283,7 +284,7 @@ export class TerminalService extends EventEmitter {
async createSession(options: TerminalOptions = {}): Promise<TerminalSession | null> {
// Check session limit
if (this.sessions.size >= maxSessions) {
console.error(`[Terminal] Max sessions (${maxSessions}) reached, refusing new session`);
logger.error(`Max sessions (${maxSessions}) reached, refusing new session`);
return null;
}
@@ -319,7 +320,7 @@ export class TerminalService extends EventEmitter {
...options.env,
};
console.log(`[Terminal] Creating session ${id} with shell: ${shell} in ${cwd}`);
logger.info(`Creating session ${id} with shell: ${shell} in ${cwd}`);
const ptyProcess = pty.spawn(shell, shellArgs, {
name: 'xterm-256color',
@@ -391,13 +392,13 @@ export class TerminalService extends EventEmitter {
// Handle exit
ptyProcess.onExit(({ exitCode }) => {
console.log(`[Terminal] Session ${id} exited with code ${exitCode}`);
logger.info(`Session ${id} exited with code ${exitCode}`);
this.sessions.delete(id);
this.exitCallbacks.forEach((cb) => cb(id, exitCode));
this.emit('exit', id, exitCode);
});
console.log(`[Terminal] Session ${id} created successfully`);
logger.info(`Session ${id} created successfully`);
return session;
}
@@ -407,7 +408,7 @@ export class TerminalService extends EventEmitter {
write(sessionId: string, data: string): boolean {
const session = this.sessions.get(sessionId);
if (!session) {
console.warn(`[Terminal] Session ${sessionId} not found`);
logger.warn(`Session ${sessionId} not found`);
return false;
}
session.pty.write(data);
@@ -422,7 +423,7 @@ export class TerminalService extends EventEmitter {
resize(sessionId: string, cols: number, rows: number, suppressOutput: boolean = true): boolean {
const session = this.sessions.get(sessionId);
if (!session) {
console.warn(`[Terminal] Session ${sessionId} not found for resize`);
logger.warn(`Session ${sessionId} not found for resize`);
return false;
}
try {
@@ -448,7 +449,7 @@ export class TerminalService extends EventEmitter {
return true;
} catch (error) {
console.error(`[Terminal] Error resizing session ${sessionId}:`, error);
logger.error(`Error resizing session ${sessionId}:`, error);
session.resizeInProgress = false; // Clear flag on error
return false;
}
@@ -476,14 +477,14 @@ export class TerminalService extends EventEmitter {
}
// First try graceful SIGTERM to allow process cleanup
console.log(`[Terminal] Session ${sessionId} sending SIGTERM`);
logger.info(`Session ${sessionId} sending SIGTERM`);
session.pty.kill('SIGTERM');
// Schedule SIGKILL fallback if process doesn't exit gracefully
// The onExit handler will remove session from map when it actually exits
setTimeout(() => {
if (this.sessions.has(sessionId)) {
console.log(`[Terminal] Session ${sessionId} still alive after SIGTERM, sending SIGKILL`);
logger.info(`Session ${sessionId} still alive after SIGTERM, sending SIGKILL`);
try {
session.pty.kill('SIGKILL');
} catch {
@@ -494,10 +495,10 @@ export class TerminalService extends EventEmitter {
}
}, 1000);
console.log(`[Terminal] Session ${sessionId} kill initiated`);
logger.info(`Session ${sessionId} kill initiated`);
return true;
} catch (error) {
console.error(`[Terminal] Error killing session ${sessionId}:`, error);
logger.error(`Error killing session ${sessionId}:`, error);
// Still try to remove from map even if kill fails
this.sessions.delete(sessionId);
return false;
@@ -580,7 +581,7 @@ export class TerminalService extends EventEmitter {
* Clean up all sessions
*/
cleanup(): void {
console.log(`[Terminal] Cleaning up ${this.sessions.size} sessions`);
logger.info(`Cleaning up ${this.sessions.size} sessions`);
this.sessions.forEach((session, id) => {
try {
// Clean up flush timeout