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,255 @@
1
+ const { spawn } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs').promises;
4
+ const logger = require('./logger');
5
+
6
+ // Timeout constants
7
+ const TIMEOUTS = {
8
+ GIT_COMMAND_MS: 30000, // 30 seconds for git operations
9
+ WORKTREE_CLEANUP_DELAY_MS: 500, // Wait after worktree removal for filesystem consistency
10
+ };
11
+
12
+ /**
13
+ * WorktreeManager - Manages git worktrees for parallel job execution
14
+ *
15
+ * Each job gets an isolated worktree as a sibling to the repo:
16
+ * <repo-parent>/<repo-name>-worktrees/job-{id}/
17
+ * with a unique branch: blaster/{slugified-task-title}-{task_id}/job-{job_id}
18
+ *
19
+ * Worktrees are created OUTSIDE the repo to prevent git conflicts.
20
+ */
21
+ class WorktreeManager {
22
+ /**
23
+ * Create a new worktree for a job with retry logic for multi-agent safety
24
+ * @param {Object} job - The job object with id, task, and project
25
+ * @param {number} maxRetries - Maximum number of retry attempts (default: 3)
26
+ * @returns {Promise<string>} - The absolute path to the worktree
27
+ */
28
+ async createWorktree(job, maxRetries = 3) {
29
+ const worktreePath = this.getWorktreePath(job)
30
+ const branchName = this.getBranchName(job)
31
+ const systemPath = job.project.system_path
32
+
33
+ // Phase 3: Use event for structured logging
34
+ logger.event('worktree.creating', {
35
+ component: 'worktree',
36
+ operation: 'create',
37
+ path: worktreePath,
38
+ branch: branchName,
39
+ systemPath
40
+ })
41
+
42
+ // Retry loop with exponential backoff for handling concurrent worktree operations
43
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
44
+ try {
45
+ // Verify git is available
46
+ await this.execGit(systemPath, ['--version'], 5000)
47
+
48
+ // Check if worktree already exists (from a previous failed run)
49
+ try {
50
+ await fs.access(worktreePath)
51
+ logger.warn(`Worktree already exists at ${worktreePath}, removing stale worktree (attempt ${attempt}/${maxRetries})`)
52
+ await this.removeWorktree(job)
53
+ // Wait a bit after removal to ensure filesystem consistency
54
+ await this.sleep(TIMEOUTS.WORKTREE_CLEANUP_DELAY_MS)
55
+ } catch (err) {
56
+ // Worktree doesn't exist, which is expected
57
+ }
58
+
59
+ // Create the worktree with a new branch
60
+ // -b creates a new branch, --detach would checkout without branch
61
+ await this.execGit(
62
+ systemPath,
63
+ ['worktree', 'add', '-b', branchName, worktreePath, 'HEAD'],
64
+ TIMEOUTS.GIT_COMMAND_MS
65
+ )
66
+
67
+ // Phase 3: Use event for structured logging
68
+ logger.event('worktree.created', {
69
+ component: 'worktree',
70
+ operation: 'create',
71
+ path: worktreePath,
72
+ branch: branchName
73
+ })
74
+ return worktreePath
75
+ } catch (error) {
76
+ const isLastAttempt = attempt === maxRetries
77
+
78
+ // Check if this is a lock/collision error that might be resolved by retrying
79
+ const isRetryableError =
80
+ error.message.includes('already exists') ||
81
+ error.message.includes('already locked') ||
82
+ error.message.includes('unable to create') ||
83
+ error.message.includes('fatal: could not lock');
84
+
85
+ if (isRetryableError && !isLastAttempt) {
86
+ // Exponential backoff: 1s, 2s, 4s
87
+ const backoffMs = 1000 * Math.pow(2, attempt - 1);
88
+ logger.warn(`Worktree creation failed (attempt ${attempt}/${maxRetries}), retrying in ${backoffMs}ms`, {
89
+ error: error.message
90
+ })
91
+ await this.sleep(backoffMs)
92
+ continue
93
+ }
94
+
95
+ // Non-retryable error or last attempt - throw
96
+ logger.error(`Failed to create worktree for job ${job.id} (attempt ${attempt}/${maxRetries})`, {
97
+ error: error.message,
98
+ worktreePath,
99
+ branchName
100
+ })
101
+ throw new Error(`Failed to create worktree: ${error.message}`)
102
+ }
103
+ }
104
+
105
+ // Should never reach here, but just in case
106
+ throw new Error('Failed to create worktree after all retry attempts')
107
+ }
108
+
109
+ /**
110
+ * Sleep helper for retry logic
111
+ * @param {number} ms - Milliseconds to sleep
112
+ */
113
+ sleep(ms) {
114
+ return new Promise(resolve => setTimeout(resolve, ms))
115
+ }
116
+
117
+ /**
118
+ * Remove a worktree after job completion
119
+ * @param {Object} job - The job object
120
+ */
121
+ async removeWorktree(job) {
122
+ const worktreePath = this.getWorktreePath(job)
123
+ const branchName = this.getBranchName(job)
124
+ const systemPath = job.project.system_path
125
+
126
+ // Phase 3: Use event for structured logging
127
+ logger.event('worktree.removing', {
128
+ component: 'worktree',
129
+ operation: 'remove',
130
+ path: worktreePath,
131
+ branch: branchName
132
+ })
133
+
134
+ try {
135
+ // Remove the worktree (--force removes even if dirty)
136
+ await this.execGit(
137
+ systemPath,
138
+ ['worktree', 'remove', worktreePath, '--force'],
139
+ TIMEOUTS.GIT_COMMAND_MS
140
+ )
141
+
142
+ // Phase 3: Use event for structured logging
143
+ logger.event('worktree.removed', {
144
+ component: 'worktree',
145
+ operation: 'remove',
146
+ path: worktreePath
147
+ })
148
+ } catch (error) {
149
+ // Log error but don't throw - cleanup is best-effort
150
+ logger.error(`Failed to remove worktree for job ${job.id}`, {
151
+ error: error.message,
152
+ worktreePath
153
+ })
154
+ }
155
+
156
+ // Note: We intentionally don't delete the branch here
157
+ // The branch remains in the repo for history/inspection
158
+ }
159
+
160
+ /**
161
+ * Get the absolute path where the worktree should be created
162
+ * Creates worktree as a sibling to the repo, not inside it
163
+ * @param {Object} job - The job object
164
+ * @returns {string} - Absolute path to worktree
165
+ */
166
+ getWorktreePath(job) {
167
+ const systemPath = job.project.system_path
168
+ const repoName = path.basename(systemPath)
169
+ const repoParent = path.dirname(systemPath)
170
+ // Create worktree as sibling: /parent/repo-worktrees/job-{id}
171
+ // NOT inside repo: /parent/repo/.ralph-worktrees/job-{id}
172
+ return path.join(repoParent, `${repoName}-worktrees`, `job-${job.id}`)
173
+ }
174
+
175
+ /**
176
+ * Get the branch name for this job
177
+ * @param {Object} job - The job object
178
+ * @returns {string} - Branch name in format blaster/ticket-{slugified-title}-{task_id}/job-{job_id}
179
+ */
180
+ getBranchName(job) {
181
+ const titleSlug = this.slugifyTitle(job.task_title || `task-${job.task_id}`)
182
+ return `blaster/${titleSlug}-${job.task_id}/job-${job.id}`
183
+ }
184
+
185
+ /**
186
+ * Convert a title to a git-safe slug
187
+ * @param {string} title - The task title
188
+ * @returns {string} - Slugified title (lowercase, alphanumeric, hyphens)
189
+ */
190
+ slugifyTitle(title) {
191
+ return title
192
+ .toLowerCase()
193
+ .trim()
194
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special chars
195
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
196
+ .replace(/-+/g, '-') // Replace multiple hyphens with single
197
+ .replace(/^-|-$/g, '') // Remove leading/trailing hyphens
198
+ .substring(0, 50) // Limit length to 50 chars
199
+ }
200
+
201
+ /**
202
+ * Execute a git command safely
203
+ * @param {string} cwd - Working directory
204
+ * @param {string[]} args - Git command arguments
205
+ * @param {number} timeout - Timeout in milliseconds (default: 30s)
206
+ * @returns {Promise<{stdout: string, stderr: string}>}
207
+ */
208
+ async execGit(cwd, args, timeout = TIMEOUTS.GIT_COMMAND_MS) {
209
+ return new Promise((resolve, reject) => {
210
+ const process = spawn('git', args, {
211
+ cwd,
212
+ stdio: ['ignore', 'pipe', 'pipe'],
213
+ shell: false // Security: Don't use shell to prevent injection
214
+ })
215
+
216
+ let stdout = ''
217
+ let stderr = ''
218
+ let timedOut = false
219
+
220
+ const timer = setTimeout(() => {
221
+ timedOut = true
222
+ process.kill('SIGTERM')
223
+ reject(new Error(`Git command timed out after ${timeout}ms`))
224
+ }, timeout)
225
+
226
+ process.stdout.on('data', (data) => {
227
+ stdout += data.toString()
228
+ })
229
+
230
+ process.stderr.on('data', (data) => {
231
+ stderr += data.toString()
232
+ })
233
+
234
+ process.on('error', (err) => {
235
+ clearTimeout(timer)
236
+ if (!timedOut) {
237
+ reject(new Error(`Failed to execute git: ${err.message}`))
238
+ }
239
+ })
240
+
241
+ process.on('close', (code) => {
242
+ clearTimeout(timer)
243
+ if (timedOut) return // Already rejected
244
+
245
+ if (code === 0) {
246
+ resolve({ stdout, stderr })
247
+ } else {
248
+ reject(new Error(`Git command failed (exit code ${code}): ${stderr || stdout}`))
249
+ }
250
+ })
251
+ })
252
+ }
253
+ }
254
+
255
+ module.exports = WorktreeManager;