ralphblaster-agent 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/bin/agent-dashboard.sh +168 -0
  4. package/bin/monitor-agent.sh +264 -0
  5. package/bin/ralphblaster.js +247 -0
  6. package/package.json +64 -0
  7. package/postinstall-colored.js +66 -0
  8. package/src/api-client.js +764 -0
  9. package/src/claude-plugin/.claude-plugin/plugin.json +9 -0
  10. package/src/claude-plugin/README.md +42 -0
  11. package/src/claude-plugin/skills/ralph/SKILL.md +259 -0
  12. package/src/commands/add-project.js +257 -0
  13. package/src/commands/init.js +79 -0
  14. package/src/config-file-manager.js +84 -0
  15. package/src/config.js +66 -0
  16. package/src/error-window.js +86 -0
  17. package/src/executor/claude-runner.js +716 -0
  18. package/src/executor/error-handler.js +65 -0
  19. package/src/executor/git-helper.js +196 -0
  20. package/src/executor/index.js +296 -0
  21. package/src/executor/job-handlers/clarifying-questions.js +213 -0
  22. package/src/executor/job-handlers/code-execution.js +145 -0
  23. package/src/executor/job-handlers/prd-generation.js +259 -0
  24. package/src/executor/path-helper.js +74 -0
  25. package/src/executor/prompt-validator.js +51 -0
  26. package/src/executor.js +4 -0
  27. package/src/index.js +342 -0
  28. package/src/logger.js +193 -0
  29. package/src/logging/README.md +93 -0
  30. package/src/logging/config.js +179 -0
  31. package/src/logging/destinations/README.md +290 -0
  32. package/src/logging/destinations/api-destination-unbatched.js +118 -0
  33. package/src/logging/destinations/api-destination.js +40 -0
  34. package/src/logging/destinations/base-destination.js +85 -0
  35. package/src/logging/destinations/batched-destination.js +198 -0
  36. package/src/logging/destinations/console-destination.js +172 -0
  37. package/src/logging/destinations/file-destination.js +208 -0
  38. package/src/logging/destinations/index.js +29 -0
  39. package/src/logging/destinations/progress-batch-destination-unbatched.js +92 -0
  40. package/src/logging/destinations/progress-batch-destination.js +41 -0
  41. package/src/logging/formatter.js +288 -0
  42. package/src/logging/log-manager.js +426 -0
  43. package/src/progress-throttle.js +101 -0
  44. package/src/system-monitor.js +64 -0
  45. package/src/utils/format.js +16 -0
  46. package/src/utils/log-file-helper.js +265 -0
  47. package/src/utils/progress-parser.js +250 -0
  48. package/src/worktree-manager.js +255 -0
@@ -0,0 +1,51 @@
1
+ const logger = require('../logger');
2
+
3
+ /**
4
+ * Validate prompt to prevent injection attacks
5
+ * @param {string} prompt - Prompt to validate
6
+ * @throws {Error} If prompt contains dangerous content
7
+ * @returns {boolean} True if validation passes
8
+ */
9
+ function validatePrompt(prompt) {
10
+ if (!prompt || typeof prompt !== 'string') {
11
+ throw new Error('Prompt must be a non-empty string');
12
+ }
13
+
14
+ // Check prompt length (prevent DoS via massive prompts)
15
+ const MAX_PROMPT_LENGTH = 500000; // 500KB
16
+ if (prompt.length > MAX_PROMPT_LENGTH) {
17
+ throw new Error(`Prompt exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`);
18
+ }
19
+
20
+ // Check for dangerous patterns that could lead to malicious operations
21
+ const dangerousPatterns = [
22
+ { pattern: /rm\s+-rf\s+\//i, description: 'dangerous deletion command' },
23
+ { pattern: /rm\s+-rf\s+~/i, description: 'dangerous home directory deletion' },
24
+ { pattern: /\/etc\/passwd/i, description: 'system file access' },
25
+ { pattern: /\/etc\/shadow/i, description: 'password file access' },
26
+ { pattern: /curl.*\|\s*sh/i, description: 'remote code execution pattern' },
27
+ { pattern: /wget.*\|\s*sh/i, description: 'remote code execution pattern' },
28
+ { pattern: /eval\s*\(/i, description: 'code evaluation' },
29
+ { pattern: /exec\s*\(/i, description: 'code execution' },
30
+ { pattern: /\$\(.*rm.*-rf/i, description: 'command injection with deletion' },
31
+ { pattern: /`.*rm.*-rf/i, description: 'command injection with deletion' },
32
+ { pattern: /base64.*decode.*eval/i, description: 'obfuscated code execution' },
33
+ { pattern: /\.ssh\/id_rsa/i, description: 'SSH key access' },
34
+ { pattern: /\.aws\/credentials/i, description: 'AWS credentials access' }
35
+ ];
36
+
37
+ for (const { pattern, description } of dangerousPatterns) {
38
+ if (pattern.test(prompt)) {
39
+ logger.error(`Prompt validation failed: contains ${description}`);
40
+ throw new Error(`Prompt contains potentially dangerous content: ${description}`);
41
+ }
42
+ }
43
+
44
+ // Log sanitized version for security audit
45
+ const sanitizedPreview = prompt.substring(0, 200).replace(/\n/g, ' ');
46
+ logger.debug(`Prompt validated (${prompt.length} chars): ${sanitizedPreview}...`);
47
+
48
+ return true;
49
+ }
50
+
51
+ module.exports = { validatePrompt };
@@ -0,0 +1,4 @@
1
+ // Backwards compatibility wrapper
2
+ // The actual Executor class has been moved to ./executor/index.js
3
+ // This file exists to maintain compatibility with existing imports
4
+ module.exports = require('./executor/index');
package/src/index.js ADDED
@@ -0,0 +1,342 @@
1
+ const ApiClient = require('./api-client');
2
+ const Executor = require('./executor');
3
+ const ProgressThrottle = require('./progress-throttle');
4
+ const ErrorWindow = require('./error-window');
5
+ const config = require('./config');
6
+ const loggingConfig = require('./logging/config');
7
+ const logger = require('./logger');
8
+ const { formatDuration } = require('./utils/format');
9
+
10
+ // Timeout constants
11
+ const TIMEOUTS = {
12
+ SHUTDOWN_DELAY_MS: 500, // Delay before shutdown
13
+ ERROR_RETRY_DELAY_MS: 5000, // 5 seconds retry delay after errors
14
+ HEARTBEAT_INTERVAL_MS: 20000, // 20 seconds for heartbeat
15
+ MIN_REQUEST_INTERVAL_MS: 1000, // Minimum 1 second between API requests
16
+ };
17
+
18
+ class RalphAgent {
19
+ constructor() {
20
+ // Agent ID for multi-agent support (from centralized logging config)
21
+ this.agentId = loggingConfig.agentId;
22
+
23
+ this.apiClient = new ApiClient(this.agentId);
24
+ this.executor = new Executor(this.apiClient);
25
+ this.isRunning = false;
26
+ this.currentJob = null;
27
+ this.heartbeatInterval = null;
28
+ this.jobStartTime = null; // Track when job started for elapsed time
29
+
30
+ // Rate limiting state
31
+ this.lastRequestTime = 0;
32
+ this.minRequestInterval = TIMEOUTS.MIN_REQUEST_INTERVAL_MS;
33
+
34
+ // Time-window based error tracking for circuit breaker
35
+ this.errorWindow = new ErrorWindow();
36
+
37
+ // Adaptive progress throttling
38
+ this.progressThrottle = new ProgressThrottle();
39
+ }
40
+
41
+ /**
42
+ * Start the agent
43
+ */
44
+ async start() {
45
+ this.isRunning = true;
46
+
47
+ // Set agent ID in logger context
48
+ logger.setAgentId(this.agentId);
49
+
50
+ logger.info('Ralph Agent starting...');
51
+ logger.info(`Agent ID: ${this.agentId}`);
52
+ logger.info(`API URL: ${config.apiUrl}`);
53
+
54
+ // Setup graceful shutdown
55
+ this.setupShutdownHandlers();
56
+
57
+ // Start polling loop
58
+ await this.pollLoop();
59
+ }
60
+
61
+ /**
62
+ * Stop the agent
63
+ */
64
+ async stop() {
65
+ logger.info('Ralph Agent stopping...');
66
+ this.isRunning = false;
67
+
68
+ // Stop heartbeat first to prevent updates during shutdown
69
+ this.stopHeartbeat();
70
+
71
+ // Kill any running Claude process
72
+ if (this.executor.currentProcess) {
73
+ logger.warn('Terminating running Claude process');
74
+ await this.executor.killCurrentProcess();
75
+ }
76
+
77
+ // If currently executing a job, mark it as failed
78
+ if (this.currentJob) {
79
+ logger.warn(`Marking job #${this.currentJob.id} as failed due to shutdown`);
80
+ try {
81
+ await this.apiClient.markJobFailed(
82
+ this.currentJob.id,
83
+ 'Agent shutdown during execution'
84
+ );
85
+ } catch (error) {
86
+ logger.error('Failed to mark job as failed during shutdown: ' + error.message);
87
+ }
88
+ }
89
+
90
+ logger.info('Ralph Agent stopped');
91
+ // Give async operations time to complete before exiting
92
+ setTimeout(() => process.exit(0), TIMEOUTS.SHUTDOWN_DELAY_MS);
93
+ }
94
+
95
+ /**
96
+ * Main polling loop with rate limiting and exponential backoff
97
+ */
98
+ async pollLoop() {
99
+ while (this.isRunning) {
100
+ try {
101
+ // Enforce minimum interval between requests
102
+ const now = Date.now();
103
+ const timeSinceLastRequest = now - this.lastRequestTime;
104
+ if (timeSinceLastRequest < this.minRequestInterval) {
105
+ await this.sleep(this.minRequestInterval - timeSinceLastRequest);
106
+ }
107
+ this.lastRequestTime = Date.now();
108
+
109
+ // Check for next job (long polling - server waits up to 10s)
110
+ const job = await this.apiClient.getNextJob();
111
+
112
+ // Note: Successful API call (error window handles its own cleanup)
113
+
114
+ if (job) {
115
+ await this.processJob(job);
116
+ // After processing, immediately poll for next job
117
+ } else {
118
+ // No jobs available after long poll timeout
119
+ // Small delay before reconnecting to prevent hammering
120
+ await this.sleep(1000); // 1s minimum between polls
121
+ }
122
+ } catch (error) {
123
+ // Record error in time window for circuit breaker
124
+ this.errorWindow.addError();
125
+ const errorCount = this.errorWindow.getErrorCount();
126
+
127
+ logger.error(`Error in poll loop (${errorCount} errors in last 60s)`, error.message);
128
+
129
+ // Exponential backoff based on recent error count: 5s, 10s, 20s, 40s, max 60s
130
+ const backoffTime = Math.min(
131
+ TIMEOUTS.ERROR_RETRY_DELAY_MS * Math.pow(2, Math.min(errorCount - 1, 4)),
132
+ 60000
133
+ );
134
+
135
+ logger.info(`Backing off for ${backoffTime}ms before retry`);
136
+ await this.sleep(backoffTime);
137
+
138
+ // Circuit breaker: Stop if error rate exceeds 50% over last 60 seconds
139
+ if (this.errorWindow.shouldShutdown()) {
140
+ logger.error('Error rate >50% in last 60 seconds, shutting down for safety');
141
+ logger.error(`Total errors: ${errorCount} (threshold based on 5s request interval)`);
142
+ await this.stop();
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Process a job
150
+ * @param {Object} job - Job object from API
151
+ */
152
+ async processJob(job) {
153
+ this.currentJob = job;
154
+ this.jobStartTime = Date.now(); // Track start time for elapsed time calculation
155
+
156
+ logger.info('═══════════════════════════════════════════════════════════');
157
+ logger.info(`Processing Job #${job.id}`, {
158
+ jobType: job.job_type,
159
+ taskTitle: job.task_title,
160
+ projectName: job.project?.name,
161
+ hasPrompt: !!job.prompt,
162
+ promptLength: job.prompt?.length || 0
163
+ });
164
+ logger.info('═══════════════════════════════════════════════════════════');
165
+
166
+ // Set logger context so internal logs are sent to UI (Phase 3: with global context)
167
+ logger.setJobContext(job.id, this.apiClient, {
168
+ jobType: job.job_type,
169
+ taskTitle: job.task_title,
170
+ projectId: job.project?.id,
171
+ projectName: job.project?.name
172
+ });
173
+
174
+ try {
175
+ // Mark job as running
176
+ logger.debug('Marking job as running...');
177
+ await this.apiClient.markJobRunning(job.id);
178
+ logger.debug('Job marked as running in API');
179
+
180
+ // Send initial status event
181
+ logger.debug('Sending initial status event to UI...');
182
+ await this.apiClient.sendStatusEvent(job.id, 'job_claimed', `Starting: ${job.task_title}`);
183
+
184
+ // Start heartbeat to keep job alive
185
+ logger.debug('Starting heartbeat timer...');
186
+ this.startHeartbeat(job.id);
187
+
188
+ // Reset progress throttle for new job
189
+ this.progressThrottle = new ProgressThrottle();
190
+
191
+ // Execute the job with progress callback
192
+ logger.info('Beginning job execution...');
193
+ const result = await this.executor.execute(job, async (chunk) => {
194
+ // Adaptive throttling: adjusts based on update rate
195
+ // High rate (>20/sec): 500ms throttle, Medium (5-20/sec): 200ms, Low (<5/sec): 100ms
196
+ if (this.progressThrottle.shouldThrottle()) {
197
+ return; // Skip this update
198
+ }
199
+
200
+ // Record this update for rate tracking
201
+ this.progressThrottle.recordUpdate();
202
+
203
+ // Send progress update to server (best-effort, don't fail job on error)
204
+ try {
205
+ await this.apiClient.sendProgress(job.id, chunk);
206
+ } catch (error) {
207
+ logger.debug(`Progress update failed for job #${job.id}: ${error.message}`);
208
+ }
209
+ });
210
+
211
+ logger.info('Job execution completed, processing results...');
212
+
213
+ // Mark job as completed
214
+ logger.info('Marking job as completed in API...');
215
+ await this.apiClient.markJobCompleted(job.id, result);
216
+
217
+ const executionTime = Date.now() - this.jobStartTime;
218
+ logger.info('═══════════════════════════════════════════════════════════');
219
+ logger.info(`✓ Job #${job.id} completed successfully`, {
220
+ executionTimeMs: executionTime,
221
+ executionTime: formatDuration(executionTime)
222
+ });
223
+ logger.info('═══════════════════════════════════════════════════════════');
224
+ } catch (error) {
225
+ const executionTime = Date.now() - this.jobStartTime;
226
+ logger.error('═══════════════════════════════════════════════════════════');
227
+ logger.error(`✗ Job #${job.id} failed after ${formatDuration(executionTime)}`, {
228
+ error: error.message,
229
+ category: error.category,
230
+ hasPartialOutput: !!error.partialOutput
231
+ });
232
+ logger.error('═══════════════════════════════════════════════════════════');
233
+
234
+ // Mark job as failed (pass full error object to include categorization)
235
+ logger.info('Marking job as failed in API...');
236
+ await this.apiClient.markJobFailed(
237
+ job.id,
238
+ error, // Pass full error object instead of just message
239
+ error.partialOutput || null
240
+ );
241
+
242
+ logger.error(`Job #${job.id} marked as failed in API`);
243
+ } finally {
244
+ // Stop heartbeat immediately to prevent race conditions
245
+ this.stopHeartbeat();
246
+ logger.info('Heartbeat stopped');
247
+
248
+ // Clear logger context (flush remaining batched logs)
249
+ logger.info('Flushing remaining logs to API...');
250
+ await logger.clearJobContext();
251
+ logger.info('Logger context cleared');
252
+
253
+ // Clear current job reference and reset time tracking
254
+ this.currentJob = null;
255
+ this.jobStartTime = null;
256
+ }
257
+ }
258
+
259
+
260
+ /**
261
+ * Start heartbeat for job
262
+ * @param {number} jobId - Job ID
263
+ */
264
+ startHeartbeat(jobId) {
265
+ // Send heartbeat every 20 seconds to maintain online status (backend offline threshold: 35s)
266
+ this.heartbeatInterval = setInterval(async () => {
267
+ try {
268
+ // Calculate elapsed time
269
+ const elapsed = Date.now() - this.jobStartTime;
270
+ const minutes = Math.floor(elapsed / 60000);
271
+ const seconds = Math.floor((elapsed % 60000) / 1000);
272
+
273
+ // Phase 1.2: Send combined heartbeat + status event in single call (reduces API calls by 50%)
274
+ await this.apiClient.sendHeartbeat(jobId, {
275
+ event_type: 'heartbeat',
276
+ message: `Still working... (${minutes}m ${seconds}s elapsed)`,
277
+ metadata: { elapsed_ms: elapsed }
278
+ });
279
+
280
+ // REMOVED: Separate sendStatusEvent() call
281
+ // await this.apiClient.sendStatusEvent(jobId, 'heartbeat', ...);
282
+ } catch (err) {
283
+ logger.warn('Heartbeat failed: ' + err.message);
284
+ }
285
+ }, TIMEOUTS.HEARTBEAT_INTERVAL_MS);
286
+ }
287
+
288
+ /**
289
+ * Stop heartbeat
290
+ */
291
+ stopHeartbeat() {
292
+ if (this.heartbeatInterval) {
293
+ clearInterval(this.heartbeatInterval);
294
+ this.heartbeatInterval = null;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Setup graceful shutdown handlers
300
+ */
301
+ setupShutdownHandlers() {
302
+ process.on('SIGINT', () => {
303
+ logger.info('Received SIGINT signal');
304
+ this.stop();
305
+ });
306
+
307
+ process.on('SIGTERM', () => {
308
+ logger.info('Received SIGTERM signal');
309
+ this.stop();
310
+ });
311
+
312
+ process.on('uncaughtException', (error) => {
313
+ logger.error('╔══════════════════════════════════════════════════════════');
314
+ logger.error('║ UNCAUGHT EXCEPTION - Agent will shutdown');
315
+ logger.error('║ Message: ' + (error?.message || error));
316
+ logger.error('║ Stack: ' + (error?.stack || 'no stack'));
317
+ logger.error('╚══════════════════════════════════════════════════════════');
318
+ console.error('UNCAUGHT EXCEPTION:', error);
319
+ this.stop();
320
+ });
321
+
322
+ process.on('unhandledRejection', (reason, promise) => {
323
+ logger.error('╔══════════════════════════════════════════════════════════');
324
+ logger.error('║ UNHANDLED REJECTION - Agent will shutdown');
325
+ logger.error('║ Reason: ' + (reason?.message || reason));
326
+ logger.error('║ Stack: ' + (reason?.stack || 'no stack'));
327
+ logger.error('╚══════════════════════════════════════════════════════════');
328
+ console.error('UNHANDLED REJECTION:', reason);
329
+ this.stop();
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Sleep helper
335
+ * @param {number} ms - Milliseconds to sleep
336
+ */
337
+ sleep(ms) {
338
+ return new Promise(resolve => setTimeout(resolve, ms));
339
+ }
340
+ }
341
+
342
+ module.exports = RalphAgent;
package/src/logger.js ADDED
@@ -0,0 +1,193 @@
1
+ const loggingConfig = require('./logging/config');
2
+ const LogManager = require('./logging/log-manager');
3
+ const { ConsoleDestination } = require('./logging/destinations');
4
+
5
+ // Create console destination with centralized config
6
+ const consoleDestination = new ConsoleDestination({
7
+ colors: loggingConfig.consoleColors,
8
+ format: loggingConfig.consoleFormat,
9
+ minLevel: loggingConfig.logLevel
10
+ });
11
+
12
+ // Initialize LogManager with console destination
13
+ // API destination will be added dynamically when job context is set
14
+ const logManager = new LogManager([consoleDestination], {
15
+ agentId: loggingConfig.agentId
16
+ });
17
+
18
+ /**
19
+ * Internal log function that delegates to LogManager
20
+ * @param {string} level - Log level ('error', 'warn', 'info', 'debug')
21
+ * @param {string} message - Log message to output
22
+ * @param {Object|null} data - Optional structured metadata to include
23
+ * @private
24
+ */
25
+ function log(level, message, data = null) {
26
+ // Delegate to LogManager (non-blocking)
27
+ logManager[level](message, data || {}).catch(() => {
28
+ // Silently handle errors to prevent cascading failures
29
+ });
30
+
31
+ // For error level, trigger immediate flush
32
+ if (level === 'error') {
33
+ logManager.flush().catch(() => {}); // Best-effort immediate flush
34
+ }
35
+ }
36
+
37
+ module.exports = {
38
+ /**
39
+ * Log an error message (level 0 - always visible)
40
+ * Used for critical errors requiring immediate attention.
41
+ * Sent to API if job context is set, and flushed immediately (doesn't wait for batch).
42
+ * @param {string} msg - Error message
43
+ * @param {Object} [data] - Optional structured metadata
44
+ * @example
45
+ * logger.error('Database connection failed', { error: err.message })
46
+ */
47
+ error: (msg, data) => log('error', msg, data),
48
+
49
+ /**
50
+ * Log a warning message (level 1)
51
+ * Used for concerning conditions that should be reviewed but aren't critical.
52
+ * Warning logs are NOT sent to the API (local only).
53
+ * @param {string} msg - Warning message
54
+ * @param {Object} [data] - Optional structured metadata
55
+ * @example
56
+ * logger.warn('Disk space low', { available: '5%' })
57
+ */
58
+ warn: (msg, data) => log('warn', msg, data),
59
+
60
+ /**
61
+ * Log an info message (level 2 - default)
62
+ * Used for general operational information.
63
+ * Sent to API if job context is set.
64
+ * @param {string} msg - Info message
65
+ * @param {Object} [data] - Optional structured metadata
66
+ * @example
67
+ * logger.info('Server started', { port: 3000 })
68
+ */
69
+ info: (msg, data) => log('info', msg, data),
70
+
71
+ /**
72
+ * Log a debug message (level 3 - most verbose)
73
+ * Used for detailed debugging information.
74
+ * Debug logs are NOT sent to the API (local only).
75
+ * @param {string} msg - Debug message
76
+ * @param {Object} [data] - Optional structured metadata
77
+ * @example
78
+ * logger.debug('Request received', { headers, body })
79
+ */
80
+ debug: (msg, data) => log('debug', msg, data),
81
+
82
+ /**
83
+ * Set agent ID for multi-agent support
84
+ * @param {string} id - Agent ID (e.g., 'agent-1', 'agent-2')
85
+ */
86
+ setAgentId: (id) => {
87
+ logManager.setAgentId(id);
88
+ },
89
+
90
+ /**
91
+ * Set job context for API logging
92
+ * When set, info and error logs will be sent to the API's "Instance Setup Logs"
93
+ * Uses batching to reduce API overhead (flushes every 2s or when 10 logs buffered)
94
+ * @param {number} jobId - Job ID
95
+ * @param {Object} apiClient - API client instance
96
+ * @param {Object} context - Optional global context to add to all logs
97
+ */
98
+ setJobContext: (jobId, apiClient, context = {}) => {
99
+ // Set job context in LogManager
100
+ logManager.setJobContext(jobId, context);
101
+ },
102
+
103
+ /**
104
+ * Clear job context (called when job completes)
105
+ * Ensures all buffered logs are flushed before shutdown
106
+ */
107
+ clearJobContext: async () => {
108
+ // Clear job context in LogManager
109
+ await logManager.clearJobContext();
110
+ },
111
+
112
+ // ===== Phase 3: Enhanced Structured Logging =====
113
+
114
+ /**
115
+ * Set global context that will be included in all subsequent logs
116
+ * @param {string|Object} keyOrContext - Key name or object with multiple keys
117
+ * @param {*} value - Value (only if first param is a string)
118
+ * @example
119
+ * logger.setContext('component', 'worktree')
120
+ * logger.setContext({ component: 'worktree', operation: 'create' })
121
+ */
122
+ setContext: (keyOrContext, value) => {
123
+ logManager.setContext(keyOrContext, value);
124
+ },
125
+
126
+ /**
127
+ * Create a child logger with additional context
128
+ * Context is additive - child inherits parent context and adds its own
129
+ * @param {Object} context - Additional context for this child logger
130
+ * @returns {Object} Child logger with all parent methods
131
+ * @example
132
+ * const worktreeLogger = logger.child({ component: 'worktree' })
133
+ * worktreeLogger.info('Creating worktree') // Includes component: 'worktree'
134
+ */
135
+ child: (context) => {
136
+ return logManager.child(context);
137
+ },
138
+
139
+ /**
140
+ * Log a semantic event with structured metadata
141
+ * @param {string} eventType - Event type in format 'category.action' (e.g., 'worktree.created')
142
+ * @param {Object} data - Additional event data
143
+ * @example
144
+ * logger.event('worktree.created', { path: '/path/to/worktree', duration: 3200 })
145
+ */
146
+ event: (eventType, data = {}) => {
147
+ logManager.event(eventType, data).catch(() => {
148
+ // Silently handle errors to prevent cascading failures
149
+ });
150
+ },
151
+
152
+ /**
153
+ * Start a performance timer for an operation
154
+ * @param {string} operation - Operation name (e.g., 'worktree.create')
155
+ * @param {Object} initialContext - Initial context for the operation
156
+ * @returns {Object} Timer object with done() method
157
+ * @example
158
+ * const timer = logger.startTimer('worktree.create')
159
+ * // ... do work ...
160
+ * timer.done({ path: '/path/to/worktree' }) // Logs duration automatically
161
+ */
162
+ startTimer: (operation, initialContext = {}) => {
163
+ return logManager.startTimer(operation, initialContext);
164
+ },
165
+
166
+ /**
167
+ * Measure and log an async operation automatically
168
+ * @param {string} operation - Operation name (e.g., 'prd.conversion')
169
+ * @param {Function} fn - Async function to measure
170
+ * @param {Object} context - Additional context
171
+ * @returns {Promise<*>} Result of the async function
172
+ * @example
173
+ * const result = await logger.measure('prd.conversion', async () => {
174
+ * return await convertPRD(job)
175
+ * })
176
+ * // Logs: prd.conversion.started, then prd.conversion.complete with duration
177
+ */
178
+ measure: async (operation, fn, context = {}) => {
179
+ return await logManager.measure(operation, fn, context);
180
+ },
181
+
182
+ // ===== Direct Access to LogManager =====
183
+
184
+ /**
185
+ * Get the underlying LogManager instance
186
+ * Useful for advanced use cases like adding/removing destinations dynamically
187
+ * @returns {LogManager} The LogManager instance
188
+ * @example
189
+ * const manager = logger.getManager()
190
+ * manager.addDestination(new FileDestination({ path: '/var/log/app.log' }))
191
+ */
192
+ getManager: () => logManager
193
+ };
@@ -0,0 +1,93 @@
1
+ # Logging Configuration
2
+
3
+ This directory contains centralized logging configuration for the RalphBlaster Agent.
4
+
5
+ ## Files
6
+
7
+ ### `config.js`
8
+
9
+ Central configuration module that consolidates all logging-related settings:
10
+
11
+ - **Console Output Settings**: `logLevel`, `consoleColors`, `consoleFormat`
12
+ - **Log Batching Settings**: `maxBatchSize`, `flushInterval`, `useBatchEndpoint`
13
+ - **Agent Identification**: `agentId`
14
+
15
+ All settings are read from environment variables with sensible defaults and validation.
16
+
17
+ ### `formatter.js`
18
+
19
+ Shared formatting utilities for log output:
20
+
21
+ - Message formatting and redaction
22
+ - Metadata formatting
23
+ - Color codes and styling
24
+
25
+ ## Usage
26
+
27
+ ### Importing Configuration
28
+
29
+ ```javascript
30
+ const loggingConfig = require('./logging/config');
31
+
32
+ console.log(loggingConfig.logLevel); // 'info'
33
+ console.log(loggingConfig.consoleColors); // true
34
+ console.log(loggingConfig.maxBatchSize); // 10
35
+ ```
36
+
37
+ ### Environment Variables
38
+
39
+ | Variable | Type | Default | Description |
40
+ |----------|------|---------|-------------|
41
+ | `RALPHBLASTER_LOG_LEVEL` | string | `info` | Log level: error, warn, info, debug |
42
+ | `RALPHBLASTER_CONSOLE_COLORS` | boolean | `true` | Enable/disable colored output |
43
+ | `RALPHBLASTER_CONSOLE_FORMAT` | string | `pretty` | Console format: pretty, json |
44
+ | `RALPHBLASTER_AGENT_ID` | string | `agent-default` | Agent identifier for multi-agent mode |
45
+ | `RALPHBLASTER_MAX_BATCH_SIZE` | number | `10` | Max logs to batch before flush |
46
+ | `RALPHBLASTER_FLUSH_INTERVAL` | number | `2000` | Milliseconds between auto-flushes |
47
+ | `RALPHBLASTER_USE_BATCH_ENDPOINT` | boolean | `true` | Use batch API endpoint |
48
+
49
+ ### Validation
50
+
51
+ The configuration module validates all values:
52
+
53
+ - **Log Level**: Must be one of `error`, `warn`, `info`, `debug` (falls back to `info`)
54
+ - **Console Format**: Must be one of `pretty`, `json` (falls back to `pretty`)
55
+ - **Numeric Values**: Must be positive integers (falls back to defaults)
56
+ - **Boolean Values**: Treats `false`, `0`, and empty string as false
57
+
58
+ Invalid values trigger a warning and use the default value.
59
+
60
+ ### Immutability
61
+
62
+ The configuration object is frozen to prevent accidental modifications:
63
+
64
+ ```javascript
65
+ loggingConfig.logLevel = 'debug'; // Fails silently in non-strict mode
66
+ console.log(loggingConfig.logLevel); // Still 'info'
67
+ ```
68
+
69
+ ## Migration Notes
70
+
71
+ Previously, logging settings were scattered across multiple files:
72
+
73
+ - `src/config.js` - logLevel, consoleColors, consoleFormat
74
+ - `src/setup-log-batcher.js` - maxBatchSize, flushInterval, useBatchEndpoint
75
+ - `src/index.js` - agentId
76
+
77
+ Now all settings are centralized in `src/logging/config.js`.
78
+
79
+ ### Backward Compatibility
80
+
81
+ For backward compatibility, `src/config.js` re-exports the console-related settings:
82
+
83
+ ```javascript
84
+ const config = require('./config');
85
+ console.log(config.logLevel); // Still works
86
+ ```
87
+
88
+ However, new code should import directly from `logging/config`:
89
+
90
+ ```javascript
91
+ const loggingConfig = require('./logging/config');
92
+ console.log(loggingConfig.logLevel); // Preferred
93
+ ```