ralphblaster-agent 1.2.0

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.
package/src/index.js ADDED
@@ -0,0 +1,243 @@
1
+ const ApiClient = require('./api-client');
2
+ const Executor = require('./executor');
3
+ const config = require('./config');
4
+ const logger = require('./logger');
5
+
6
+ // Timing constants
7
+ const SHUTDOWN_DELAY_MS = 500;
8
+ const ERROR_RETRY_DELAY_MS = 5000;
9
+ const HEARTBEAT_INTERVAL_MS = 60000;
10
+
11
+ class RalphAgent {
12
+ constructor() {
13
+ this.apiClient = new ApiClient();
14
+ this.executor = new Executor(this.apiClient);
15
+ this.isRunning = false;
16
+ this.currentJob = null;
17
+ this.heartbeatInterval = null;
18
+ this.jobCompleting = false; // Flag to prevent heartbeat race conditions
19
+
20
+ // Rate limiting state
21
+ this.consecutiveErrors = 0;
22
+ this.lastRequestTime = 0;
23
+ this.minRequestInterval = 1000; // Minimum 1s between requests
24
+ }
25
+
26
+ /**
27
+ * Start the agent
28
+ */
29
+ async start() {
30
+ this.isRunning = true;
31
+
32
+ logger.info('Ralph Agent starting...');
33
+ logger.info(`API URL: ${config.apiUrl}`);
34
+
35
+ // Setup graceful shutdown
36
+ this.setupShutdownHandlers();
37
+
38
+ // Start polling loop
39
+ await this.pollLoop();
40
+ }
41
+
42
+ /**
43
+ * Stop the agent
44
+ */
45
+ async stop() {
46
+ logger.info('Ralph Agent stopping...');
47
+ this.isRunning = false;
48
+
49
+ // Stop heartbeat first to prevent updates during shutdown
50
+ this.stopHeartbeat();
51
+
52
+ // Kill any running Claude process
53
+ if (this.executor.currentProcess) {
54
+ logger.warn('Terminating running Claude process');
55
+ await this.executor.killCurrentProcess();
56
+ }
57
+
58
+ // If currently executing a job, mark it as failed
59
+ if (this.currentJob) {
60
+ logger.warn(`Marking job #${this.currentJob.id} as failed due to shutdown`);
61
+ try {
62
+ await this.apiClient.markJobFailed(
63
+ this.currentJob.id,
64
+ 'Agent shutdown during execution'
65
+ );
66
+ } catch (error) {
67
+ logger.error('Failed to mark job as failed during shutdown', error.message);
68
+ }
69
+ }
70
+
71
+ logger.info('Ralph Agent stopped');
72
+ // Give async operations time to complete before exiting
73
+ setTimeout(() => process.exit(0), SHUTDOWN_DELAY_MS);
74
+ }
75
+
76
+ /**
77
+ * Main polling loop with rate limiting and exponential backoff
78
+ */
79
+ async pollLoop() {
80
+ while (this.isRunning) {
81
+ try {
82
+ // Enforce minimum interval between requests
83
+ const now = Date.now();
84
+ const timeSinceLastRequest = now - this.lastRequestTime;
85
+ if (timeSinceLastRequest < this.minRequestInterval) {
86
+ await this.sleep(this.minRequestInterval - timeSinceLastRequest);
87
+ }
88
+ this.lastRequestTime = Date.now();
89
+
90
+ // Check for next job (long polling - server waits up to 30s)
91
+ const job = await this.apiClient.getNextJob();
92
+
93
+ // Reset error counter on successful API call
94
+ this.consecutiveErrors = 0;
95
+
96
+ if (job) {
97
+ await this.processJob(job);
98
+ // After processing, immediately poll for next job
99
+ } else {
100
+ // No jobs available after long poll timeout
101
+ // Small delay before reconnecting to prevent hammering
102
+ await this.sleep(1000); // 1s minimum between polls
103
+ }
104
+ } catch (error) {
105
+ this.consecutiveErrors++;
106
+ logger.error(`Error in poll loop (consecutive: ${this.consecutiveErrors})`, error.message);
107
+
108
+ // Exponential backoff: 5s, 10s, 20s, 40s, max 60s
109
+ const backoffTime = Math.min(
110
+ ERROR_RETRY_DELAY_MS * Math.pow(2, this.consecutiveErrors - 1),
111
+ 60000
112
+ );
113
+
114
+ logger.info(`Backing off for ${backoffTime}ms before retry`);
115
+ await this.sleep(backoffTime);
116
+
117
+ // Circuit breaker: Stop if too many consecutive errors
118
+ if (this.consecutiveErrors >= 10) {
119
+ logger.error('Too many consecutive errors (10+), shutting down');
120
+ await this.stop();
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Process a job
128
+ * @param {Object} job - Job object from API
129
+ */
130
+ async processJob(job) {
131
+ this.currentJob = job;
132
+ this.jobCompleting = false;
133
+
134
+ try {
135
+ // Mark job as running
136
+ await this.apiClient.markJobRunning(job.id);
137
+
138
+ // Start heartbeat to keep job alive
139
+ this.startHeartbeat(job.id);
140
+
141
+ // Execute the job with progress callback
142
+ const result = await this.executor.execute(job, async (chunk) => {
143
+ // Send progress update to server
144
+ try {
145
+ await this.apiClient.sendProgress(job.id, chunk);
146
+ } catch (error) {
147
+ logger.warn(`Failed to send progress update for job #${job.id}`, error.message);
148
+ // Don't fail the job if progress update fails
149
+ }
150
+ });
151
+
152
+ // Set flag to prevent heartbeat race conditions, then stop heartbeat
153
+ this.jobCompleting = true;
154
+ this.stopHeartbeat();
155
+
156
+ // Mark job as completed
157
+ await this.apiClient.markJobCompleted(job.id, result);
158
+
159
+ logger.info(`Job #${job.id} completed successfully`);
160
+ } catch (error) {
161
+ // Set flag to prevent heartbeat race conditions, then stop heartbeat
162
+ this.jobCompleting = true;
163
+ this.stopHeartbeat();
164
+
165
+ // Mark job as failed (pass full error object to include categorization)
166
+ await this.apiClient.markJobFailed(
167
+ job.id,
168
+ error, // Pass full error object instead of just message
169
+ error.partialOutput || null
170
+ );
171
+
172
+ logger.error(`Job #${job.id} failed`, error.message);
173
+ } finally {
174
+ // Clear current job reference and reset completion flag
175
+ this.currentJob = null;
176
+ this.jobCompleting = false;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Start heartbeat for job
182
+ * @param {number} jobId - Job ID
183
+ */
184
+ startHeartbeat(jobId) {
185
+ // Send heartbeat every 60 seconds to prevent timeout
186
+ this.heartbeatInterval = setInterval(() => {
187
+ // Check if job is completing to prevent race conditions
188
+ if (this.jobCompleting) {
189
+ logger.debug('Skipping heartbeat - job is completing');
190
+ return;
191
+ }
192
+
193
+ this.apiClient.sendHeartbeat(jobId).catch(err => {
194
+ logger.warn('Heartbeat failed', err.message);
195
+ });
196
+ }, HEARTBEAT_INTERVAL_MS);
197
+ }
198
+
199
+ /**
200
+ * Stop heartbeat
201
+ */
202
+ stopHeartbeat() {
203
+ if (this.heartbeatInterval) {
204
+ clearInterval(this.heartbeatInterval);
205
+ this.heartbeatInterval = null;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Setup graceful shutdown handlers
211
+ */
212
+ setupShutdownHandlers() {
213
+ process.on('SIGINT', () => {
214
+ logger.info('Received SIGINT signal');
215
+ this.stop();
216
+ });
217
+
218
+ process.on('SIGTERM', () => {
219
+ logger.info('Received SIGTERM signal');
220
+ this.stop();
221
+ });
222
+
223
+ process.on('uncaughtException', (error) => {
224
+ logger.error('Uncaught exception', error);
225
+ this.stop();
226
+ });
227
+
228
+ process.on('unhandledRejection', (reason, promise) => {
229
+ logger.error('Unhandled rejection', reason);
230
+ this.stop();
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Sleep helper
236
+ * @param {number} ms - Milliseconds to sleep
237
+ */
238
+ sleep(ms) {
239
+ return new Promise(resolve => setTimeout(resolve, ms));
240
+ }
241
+ }
242
+
243
+ module.exports = RalphAgent;
package/src/logger.js ADDED
@@ -0,0 +1,96 @@
1
+ const config = require('./config');
2
+
3
+ const LOG_LEVELS = {
4
+ error: 0,
5
+ warn: 1,
6
+ info: 2,
7
+ debug: 3
8
+ };
9
+
10
+ const currentLevel = LOG_LEVELS[config.logLevel] || LOG_LEVELS.info;
11
+
12
+ /**
13
+ * Redact sensitive data from logs
14
+ * @param {any} data - Data to redact
15
+ * @returns {any} Redacted data
16
+ */
17
+ function redactSensitiveData(data) {
18
+ if (!data) return data;
19
+
20
+ try {
21
+ // Convert to string for pattern matching
22
+ let dataStr = typeof data === 'string' ? data : JSON.stringify(data);
23
+
24
+ // Redact common token patterns
25
+ dataStr = dataStr
26
+ .replace(/"Authorization":\s*"Bearer [^"]+"/g, '"Authorization": "Bearer [REDACTED]"')
27
+ .replace(/Authorization:\s*Bearer\s+[^\s,}]+/g, 'Authorization: Bearer [REDACTED]')
28
+ .replace(/RALPH_API_TOKEN=[^\s&]+/g, 'RALPH_API_TOKEN=[REDACTED]')
29
+ .replace(/"apiToken":\s*"[^"]+"/g, '"apiToken": "[REDACTED]"')
30
+ .replace(/"token":\s*"[^"]+"/g, '"token": "[REDACTED]"')
31
+ .replace(/"api_token":\s*"[^"]+"/g, '"api_token": "[REDACTED]"')
32
+ .replace(/Bearer\s+[A-Za-z0-9_-]{20,}/g, 'Bearer [REDACTED]');
33
+
34
+ // Return in original format
35
+ if (typeof data === 'string') {
36
+ return dataStr;
37
+ } else {
38
+ try {
39
+ return JSON.parse(dataStr);
40
+ } catch {
41
+ return dataStr; // Return string if can't parse back
42
+ }
43
+ }
44
+ } catch (error) {
45
+ // If redaction fails, return safe placeholder
46
+ return '[REDACTION_ERROR]';
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Safe JSON stringify that handles circular references
52
+ * @param {*} obj - Object to stringify
53
+ * @returns {string} JSON string or error message
54
+ */
55
+ function safeStringify(obj) {
56
+ const seen = new WeakSet();
57
+ try {
58
+ return JSON.stringify(obj, (key, value) => {
59
+ // Handle circular references
60
+ if (typeof value === 'object' && value !== null) {
61
+ if (seen.has(value)) {
62
+ return '[Circular]';
63
+ }
64
+ seen.add(value);
65
+ }
66
+ return value;
67
+ }, 2);
68
+ } catch (error) {
69
+ return `[Unable to stringify: ${error.message}]`;
70
+ }
71
+ }
72
+
73
+ function log(level, message, data = null) {
74
+ if (LOG_LEVELS[level] <= currentLevel) {
75
+ const timestamp = new Date().toISOString();
76
+ const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
77
+
78
+ // Redact sensitive data from message
79
+ const safeMessage = redactSensitiveData(message);
80
+
81
+ if (data) {
82
+ // Redact and stringify data
83
+ const redactedData = redactSensitiveData(data);
84
+ console.log(prefix, safeMessage, safeStringify(redactedData));
85
+ } else {
86
+ console.log(prefix, safeMessage);
87
+ }
88
+ }
89
+ }
90
+
91
+ module.exports = {
92
+ error: (msg, data) => log('error', msg, data),
93
+ warn: (msg, data) => log('warn', msg, data),
94
+ info: (msg, data) => log('info', msg, data),
95
+ debug: (msg, data) => log('debug', msg, data)
96
+ };
@@ -0,0 +1,165 @@
1
+ # Ralph Agent Instructions
2
+
3
+ You are an autonomous coding agent working on a software project.
4
+
5
+ ## CRITICAL: Worktree Awareness
6
+
7
+ This Ralph instance works in an isolated git worktree, NOT the main repository.
8
+
9
+ **Environment variables (always available):**
10
+ - `RALPH_WORKTREE_PATH`: Your worktree directory where code lives (e.g., ~/src/myproject-worktrees/feature-name)
11
+ - `RALPH_INSTANCE_DIR`: Instance directory where state files live (e.g., ~/src/myproject/ralph-instances/prd-feature)
12
+ - `RALPH_MAIN_REPO`: Main repository path (e.g., ~/src/myproject)
13
+
14
+ **File locations:**
15
+ - **Code files**: Work in `$RALPH_WORKTREE_PATH` - this is your working directory
16
+ - **State files**: Read/write from `$RALPH_INSTANCE_DIR`:
17
+ - `$RALPH_INSTANCE_DIR/prd.json` - Your task configuration
18
+ - `$RALPH_INSTANCE_DIR/progress.txt` - Your progress log
19
+
20
+ **Working directory rules:**
21
+ 1. ALWAYS run code/git operations from worktree: `cd $RALPH_WORKTREE_PATH`
22
+ 2. Read/write state files using absolute paths (the environment variables above)
23
+
24
+ ## Permissions
25
+
26
+ You have FULL PERMISSION to:
27
+ - Edit ANY files in the repository (no need to ask for approval)
28
+ - Write new files as required by user stories
29
+ - Run bash commands for git operations, quality checks, testing, etc.
30
+ - Delete files if necessary for completing user stories
31
+
32
+ DO NOT wait for permission or approval - you are authorized to make all necessary changes autonomously.
33
+
34
+ ## Your Task
35
+
36
+ 1. Read the PRD at `$RALPH_INSTANCE_DIR/prd.json`
37
+ 2. Read the progress log at `$RALPH_INSTANCE_DIR/progress.txt` (check Codebase Patterns section first)
38
+ 3. Navigate to worktree: `cd $RALPH_WORKTREE_PATH` (SKIP branch checkout - you're already on the correct branch!)
39
+ 4. Pick the **highest priority** user story where `passes: false`
40
+ 5. Implement that single user story
41
+ 6. Run quality checks (e.g., typecheck, lint, test - use whatever your project requires)
42
+ 7. Update AGENTS.md files if you discover reusable patterns (see below)
43
+ 8. If checks pass, commit ALL changes with message: `feat: [Story ID] - [Story Title]`
44
+ 9. Update the PRD at `$RALPH_INSTANCE_DIR/prd.json` to set `passes: true` for the completed story
45
+ 10. Append your progress to `$RALPH_INSTANCE_DIR/progress.txt`
46
+
47
+ ## Example Commands
48
+
49
+ ```bash
50
+ # Read configuration
51
+ cat $RALPH_INSTANCE_DIR/prd.json
52
+
53
+ # Navigate to worktree
54
+ cd $RALPH_WORKTREE_PATH
55
+
56
+ # Check git status
57
+ git status
58
+
59
+ # Make code changes (you're in the worktree)
60
+ # ... edit files, run tests, etc ...
61
+
62
+ # Run quality checks
63
+ npm run typecheck
64
+ npm test
65
+
66
+ # Commit changes
67
+ git add .
68
+ git commit -m "feat: US-001 - Add feature"
69
+
70
+ # Update state files (use absolute paths with environment variables!)
71
+ # Use Edit or Write tool with: $RALPH_INSTANCE_DIR/prd.json
72
+ # Use Edit or Write tool with: $RALPH_INSTANCE_DIR/progress.txt
73
+ ```
74
+
75
+ ## Progress Report Format
76
+
77
+ APPEND to progress.txt (never replace, always append):
78
+ ```
79
+ ## [Date/Time] - [Story ID]
80
+ Thread: https://ampcode.com/threads/$AMP_CURRENT_THREAD_ID
81
+ - What was implemented
82
+ - Files changed
83
+ - **Learnings for future iterations:**
84
+ - Patterns discovered (e.g., "this codebase uses X for Y")
85
+ - Gotchas encountered (e.g., "don't forget to update Z when changing W")
86
+ - Useful context (e.g., "the evaluation panel is in component X")
87
+ ---
88
+ ```
89
+
90
+ Include the thread URL so future iterations can use the `read_thread` tool to reference previous work if needed.
91
+
92
+ The learnings section is critical - it helps future iterations avoid repeating mistakes and understand the codebase better.
93
+
94
+ ## Consolidate Patterns
95
+
96
+ If you discover a **reusable pattern** that future iterations should know, add it to the `## Codebase Patterns` section at the TOP of progress.txt (create it if it doesn't exist). This section should consolidate the most important learnings:
97
+
98
+ ```
99
+ ## Codebase Patterns
100
+ - Example: Use `sql<number>` template for aggregations
101
+ - Example: Always use `IF NOT EXISTS` for migrations
102
+ - Example: Export types from actions.ts for UI components
103
+ ```
104
+
105
+ Only add patterns that are **general and reusable**, not story-specific details.
106
+
107
+ ## Update AGENTS.md Files
108
+
109
+ Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files:
110
+
111
+ 1. **Identify directories with edited files** - Look at which directories you modified
112
+ 2. **Check for existing AGENTS.md** - Look for AGENTS.md in those directories or parent directories
113
+ 3. **Add valuable learnings** - If you discovered something future developers/agents should know:
114
+ - API patterns or conventions specific to that module
115
+ - Gotchas or non-obvious requirements
116
+ - Dependencies between files
117
+ - Testing approaches for that area
118
+ - Configuration or environment requirements
119
+
120
+ **Examples of good AGENTS.md additions:**
121
+ - "When modifying X, also update Y to keep them in sync"
122
+ - "This module uses pattern Z for all API calls"
123
+ - "Tests require the dev server running on PORT 3000"
124
+ - "Field names must match the template exactly"
125
+
126
+ **Do NOT add:**
127
+ - Story-specific implementation details
128
+ - Temporary debugging notes
129
+ - Information already in progress.txt
130
+
131
+ Only update AGENTS.md if you have **genuinely reusable knowledge** that would help future work in that directory.
132
+
133
+ ## Quality Requirements
134
+
135
+ - ALL commits must pass your project's quality checks (typecheck, lint, test)
136
+ - Do NOT commit broken code
137
+ - Keep changes focused and minimal
138
+ - Follow existing code patterns
139
+
140
+ ## Browser Testing (Required for Frontend Stories)
141
+
142
+ For any story that changes UI, you MUST verify it works in the browser:
143
+
144
+ 1. Load the `dev-browser` skill
145
+ 2. Navigate to the relevant page
146
+ 3. Verify the UI changes work as expected
147
+ 4. Take a screenshot if helpful for the progress log
148
+
149
+ A frontend story is NOT complete until browser verification passes.
150
+
151
+ ## Stop Condition
152
+
153
+ After completing a user story, check if ALL stories have `passes: true`.
154
+
155
+ If ALL stories are complete and passing, reply with:
156
+ <promise>COMPLETE</promise>
157
+
158
+ If there are still stories with `passes: false`, end your response normally (another iteration will pick up the next story).
159
+
160
+ ## Important
161
+
162
+ - Work on ONE story per iteration
163
+ - Commit frequently
164
+ - Keep CI green
165
+ - Read the Codebase Patterns section in progress.txt before starting