coder-config 0.40.16 → 0.41.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.
@@ -19,8 +19,8 @@
19
19
 
20
20
  <!-- PWA Manifest -->
21
21
  <link rel="manifest" href="/manifest.json">
22
- <script type="module" crossorigin src="/assets/index-DZrd_FEC.js"></script>
23
- <link rel="stylesheet" crossorigin href="/assets/index-DjLdm3Mr.css">
22
+ <script type="module" crossorigin src="/assets/index-CGdqBV9k.js"></script>
23
+ <link rel="stylesheet" crossorigin href="/assets/index-BX3EJoIY.css">
24
24
  </head>
25
25
  <body>
26
26
  <div id="root"></div>
@@ -4,6 +4,7 @@
4
4
 
5
5
  const projects = require('./projects');
6
6
  const workstreams = require('./workstreams');
7
+ const loops = require('./loops');
7
8
  const activity = require('./activity');
8
9
  const subprojects = require('./subprojects');
9
10
  const registry = require('./registry');
@@ -23,6 +24,7 @@ const mcpDiscovery = require('./mcp-discovery');
23
24
  module.exports = {
24
25
  projects,
25
26
  workstreams,
27
+ loops,
26
28
  activity,
27
29
  subprojects,
28
30
  registry,
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Loops Routes (Ralph Loop)
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ /**
10
+ * Get all loops
11
+ */
12
+ function getLoops(manager) {
13
+ if (!manager) return { error: 'Manager not available' };
14
+ const data = manager.loadLoops();
15
+
16
+ // Enrich with state data
17
+ const loops = (data.loops || []).map(loop => {
18
+ const state = manager.loadLoopState(loop.id);
19
+ return state || loop;
20
+ });
21
+
22
+ return {
23
+ loops,
24
+ activeId: data.activeId,
25
+ config: data.config || {}
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Get active loop
31
+ */
32
+ function getActiveLoop(manager) {
33
+ if (!manager) return { error: 'Manager not available' };
34
+ const active = manager.getActiveLoop();
35
+ return { loop: active };
36
+ }
37
+
38
+ /**
39
+ * Get a specific loop by ID
40
+ */
41
+ function getLoop(manager, id) {
42
+ if (!manager) return { error: 'Manager not available' };
43
+ const loop = manager.loopGet(id);
44
+ if (!loop) {
45
+ return { error: 'Loop not found' };
46
+ }
47
+
48
+ // Also load clarifications and plan
49
+ const clarifications = manager.loadClarifications(loop.id);
50
+ const plan = manager.loadPlan(loop.id);
51
+
52
+ return {
53
+ loop,
54
+ clarifications,
55
+ plan
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create a new loop
61
+ */
62
+ function createLoop(manager, body) {
63
+ if (!manager) return { error: 'Manager not available' };
64
+ const { task, name, workstreamId, projectPath } = body;
65
+
66
+ if (!task) {
67
+ return { error: 'Task description is required' };
68
+ }
69
+
70
+ const loop = manager.loopCreate(task, {
71
+ name,
72
+ workstreamId,
73
+ projectPath
74
+ });
75
+
76
+ if (!loop) {
77
+ return { error: 'Failed to create loop' };
78
+ }
79
+
80
+ return { success: true, loop };
81
+ }
82
+
83
+ /**
84
+ * Update a loop
85
+ */
86
+ function updateLoop(manager, id, updates) {
87
+ if (!manager) return { error: 'Manager not available' };
88
+ const loop = manager.loopUpdate(id, updates);
89
+ if (!loop) {
90
+ return { error: 'Loop not found' };
91
+ }
92
+ return { success: true, loop };
93
+ }
94
+
95
+ /**
96
+ * Delete a loop
97
+ */
98
+ function deleteLoop(manager, id) {
99
+ if (!manager) return { error: 'Manager not available' };
100
+ const success = manager.loopDelete(id);
101
+ if (!success) {
102
+ return { error: 'Loop not found' };
103
+ }
104
+ return { success: true };
105
+ }
106
+
107
+ /**
108
+ * Start a loop
109
+ */
110
+ function startLoop(manager, id) {
111
+ if (!manager) return { error: 'Manager not available' };
112
+ const loop = manager.loopStart(id);
113
+ if (!loop) {
114
+ return { error: 'Loop not found or cannot be started' };
115
+ }
116
+ return { success: true, loop };
117
+ }
118
+
119
+ /**
120
+ * Pause a loop
121
+ */
122
+ function pauseLoop(manager, id) {
123
+ if (!manager) return { error: 'Manager not available' };
124
+ const loop = manager.loopPause(id);
125
+ if (!loop) {
126
+ return { error: 'Loop not found' };
127
+ }
128
+ return { success: true, loop };
129
+ }
130
+
131
+ /**
132
+ * Resume a loop
133
+ */
134
+ function resumeLoop(manager, id) {
135
+ if (!manager) return { error: 'Manager not available' };
136
+ const loop = manager.loopResume(id);
137
+ if (!loop) {
138
+ return { error: 'Loop not found or cannot be resumed' };
139
+ }
140
+ return { success: true, loop };
141
+ }
142
+
143
+ /**
144
+ * Cancel a loop
145
+ */
146
+ function cancelLoop(manager, id) {
147
+ if (!manager) return { error: 'Manager not available' };
148
+ const loop = manager.loopCancel(id);
149
+ if (!loop) {
150
+ return { error: 'Loop not found' };
151
+ }
152
+ return { success: true, loop };
153
+ }
154
+
155
+ /**
156
+ * Approve plan (phase 2)
157
+ */
158
+ function approveLoop(manager, id) {
159
+ if (!manager) return { error: 'Manager not available' };
160
+ const loop = manager.loopApprove(id);
161
+ if (!loop) {
162
+ return { error: 'Loop not found or not in plan phase' };
163
+ }
164
+ return { success: true, loop };
165
+ }
166
+
167
+ /**
168
+ * Mark loop as complete
169
+ */
170
+ function completeLoop(manager, id) {
171
+ if (!manager) return { error: 'Manager not available' };
172
+ const loop = manager.loopComplete(id);
173
+ if (!loop) {
174
+ return { error: 'Loop not found' };
175
+ }
176
+ return { success: true, loop };
177
+ }
178
+
179
+ /**
180
+ * Get loop history
181
+ */
182
+ function getLoopHistory(manager) {
183
+ if (!manager) return { error: 'Manager not available' };
184
+ const history = manager.loadHistory();
185
+ return { completed: history.completed || [] };
186
+ }
187
+
188
+ /**
189
+ * Get/update loop configuration
190
+ */
191
+ function getLoopConfig(manager) {
192
+ if (!manager) return { error: 'Manager not available' };
193
+ const data = manager.loadLoops();
194
+ return { config: data.config || {} };
195
+ }
196
+
197
+ function updateLoopConfig(manager, updates) {
198
+ if (!manager) return { error: 'Manager not available' };
199
+ const config = manager.loopConfig(updates);
200
+ return { success: true, config };
201
+ }
202
+
203
+ /**
204
+ * Save clarifications to a loop
205
+ */
206
+ function saveClarifications(manager, id, content) {
207
+ if (!manager) return { error: 'Manager not available' };
208
+ const loop = manager.loopGet(id);
209
+ if (!loop) {
210
+ return { error: 'Loop not found' };
211
+ }
212
+ manager.saveClarifications(id, content);
213
+ return { success: true };
214
+ }
215
+
216
+ /**
217
+ * Save plan to a loop
218
+ */
219
+ function savePlan(manager, id, content) {
220
+ if (!manager) return { error: 'Manager not available' };
221
+ const loop = manager.loopGet(id);
222
+ if (!loop) {
223
+ return { error: 'Loop not found' };
224
+ }
225
+ manager.savePlan(id, content);
226
+ return { success: true };
227
+ }
228
+
229
+ /**
230
+ * Record an iteration
231
+ */
232
+ function recordIteration(manager, id, iteration) {
233
+ if (!manager) return { error: 'Manager not available' };
234
+ const loop = manager.recordIteration(id, iteration);
235
+ if (!loop) {
236
+ return { error: 'Loop not found' };
237
+ }
238
+ return { success: true, loop };
239
+ }
240
+
241
+ /**
242
+ * Check if loop hooks are installed
243
+ */
244
+ function getLoopHookStatus() {
245
+ const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
246
+ const stopHookPath = path.join(hooksDir, 'ralph-loop-stop.sh');
247
+ const prepromptHookPath = path.join(hooksDir, 'ralph-loop-preprompt.sh');
248
+
249
+ const status = {
250
+ hooksDir,
251
+ dirExists: fs.existsSync(hooksDir),
252
+ stopHook: {
253
+ path: stopHookPath,
254
+ exists: fs.existsSync(stopHookPath)
255
+ },
256
+ prepromptHook: {
257
+ path: prepromptHookPath,
258
+ exists: fs.existsSync(prepromptHookPath)
259
+ }
260
+ };
261
+
262
+ return status;
263
+ }
264
+
265
+ /**
266
+ * Install loop hooks
267
+ */
268
+ function installLoopHooks(manager) {
269
+ const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
270
+ const coderConfigDir = manager ? path.dirname(manager.getLoopsPath()) : path.join(os.homedir(), '.coder-config');
271
+
272
+ try {
273
+ if (!fs.existsSync(hooksDir)) {
274
+ fs.mkdirSync(hooksDir, { recursive: true });
275
+ }
276
+
277
+ // Stop hook
278
+ const stopHookContent = `#!/bin/bash
279
+ # Ralph Loop continuation hook
280
+ # Called after each Claude response
281
+
282
+ LOOP_ID="\$CODER_LOOP_ID"
283
+ if [[ -z "\$LOOP_ID" ]]; then
284
+ exit 0
285
+ fi
286
+
287
+ STATE_FILE="\$HOME/.coder-config/loops/\$LOOP_ID/state.json"
288
+ if [[ ! -f "\$STATE_FILE" ]]; then
289
+ exit 0
290
+ fi
291
+
292
+ # Check if loop is still active
293
+ STATUS=$(jq -r '.status' "\$STATE_FILE")
294
+ if [[ "\$STATUS" != "running" ]]; then
295
+ exit 0
296
+ fi
297
+
298
+ # Check budget limits
299
+ CURRENT_ITER=$(jq -r '.iterations.current' "\$STATE_FILE")
300
+ MAX_ITER=$(jq -r '.iterations.max' "\$STATE_FILE")
301
+ CURRENT_COST=$(jq -r '.budget.currentCost' "\$STATE_FILE")
302
+ MAX_COST=$(jq -r '.budget.maxCost' "\$STATE_FILE")
303
+
304
+ if (( CURRENT_ITER >= MAX_ITER )); then
305
+ echo "Loop paused: max iterations reached (\$MAX_ITER)"
306
+ jq '.status = "paused" | .pauseReason = "max_iterations"' "\$STATE_FILE" > tmp && mv tmp "\$STATE_FILE"
307
+ exit 0
308
+ fi
309
+
310
+ if (( $(echo "\$CURRENT_COST >= \$MAX_COST" | bc -l) )); then
311
+ echo "Loop paused: budget exceeded (\$\$MAX_COST)"
312
+ jq '.status = "paused" | .pauseReason = "budget"' "\$STATE_FILE" > tmp && mv tmp "\$STATE_FILE"
313
+ exit 0
314
+ fi
315
+
316
+ # Update iteration count
317
+ jq ".iterations.current = $((CURRENT_ITER + 1))" "\$STATE_FILE" > tmp && mv tmp "\$STATE_FILE"
318
+
319
+ # Check if task is complete (Claude sets this flag)
320
+ PHASE=$(jq -r '.phase' "\$STATE_FILE")
321
+ TASK_COMPLETE=$(jq -r '.taskComplete // false' "\$STATE_FILE")
322
+
323
+ if [[ "\$TASK_COMPLETE" == "true" ]]; then
324
+ jq '.status = "completed" | .completedAt = now' "\$STATE_FILE" > tmp && mv tmp "\$STATE_FILE"
325
+ echo "Loop completed successfully!"
326
+ exit 0
327
+ fi
328
+
329
+ # Continue loop - output continuation prompt
330
+ PHASE_PROMPT=""
331
+ case "\$PHASE" in
332
+ "clarify")
333
+ PHASE_PROMPT="Continue clarifying requirements. If requirements are clear, advance to planning phase by setting phase='plan'."
334
+ ;;
335
+ "plan")
336
+ PHASE_PROMPT="Continue developing the implementation plan. When plan is complete and approved, advance to execution phase."
337
+ ;;
338
+ "execute")
339
+ PHASE_PROMPT="Continue executing the plan. When task is complete, set taskComplete=true."
340
+ ;;
341
+ esac
342
+
343
+ echo ""
344
+ echo "---"
345
+ echo "[Ralph Loop iteration $((CURRENT_ITER + 1))/\$MAX_ITER]"
346
+ echo "\$PHASE_PROMPT"
347
+ echo "---"
348
+ `;
349
+
350
+ const stopHookPath = path.join(hooksDir, 'ralph-loop-stop.sh');
351
+ fs.writeFileSync(stopHookPath, stopHookContent);
352
+ fs.chmodSync(stopHookPath, '755');
353
+
354
+ // Pre-prompt hook
355
+ const prepromptHookContent = `#!/bin/bash
356
+ # Ralph Loop context injection
357
+
358
+ LOOP_ID="\$CODER_LOOP_ID"
359
+ if [[ -z "\$LOOP_ID" ]]; then
360
+ exit 0
361
+ fi
362
+
363
+ STATE_FILE="\$HOME/.coder-config/loops/\$LOOP_ID/state.json"
364
+ PLAN_FILE="\$HOME/.coder-config/loops/\$LOOP_ID/plan.md"
365
+ CLARIFY_FILE="\$HOME/.coder-config/loops/\$LOOP_ID/clarifications.md"
366
+
367
+ if [[ ! -f "\$STATE_FILE" ]]; then
368
+ exit 0
369
+ fi
370
+
371
+ echo "<ralph-loop-context>"
372
+ echo "Loop: $(jq -r '.name' "\$STATE_FILE")"
373
+ echo "Phase: $(jq -r '.phase' "\$STATE_FILE")"
374
+ echo "Iteration: $(jq -r '.iterations.current' "\$STATE_FILE")/$(jq -r '.iterations.max' "\$STATE_FILE")"
375
+
376
+ if [[ -f "\$CLARIFY_FILE" ]]; then
377
+ echo ""
378
+ echo "## Clarifications"
379
+ cat "\$CLARIFY_FILE"
380
+ fi
381
+
382
+ if [[ -f "\$PLAN_FILE" ]]; then
383
+ echo ""
384
+ echo "## Plan"
385
+ cat "\$PLAN_FILE"
386
+ fi
387
+
388
+ echo "</ralph-loop-context>"
389
+ `;
390
+
391
+ const prepromptHookPath = path.join(hooksDir, 'ralph-loop-preprompt.sh');
392
+ fs.writeFileSync(prepromptHookPath, prepromptHookContent);
393
+ fs.chmodSync(prepromptHookPath, '755');
394
+
395
+ return {
396
+ success: true,
397
+ message: 'Loop hooks installed successfully',
398
+ stopHook: stopHookPath,
399
+ prepromptHook: prepromptHookPath
400
+ };
401
+ } catch (e) {
402
+ return { error: e.message };
403
+ }
404
+ }
405
+
406
+ module.exports = {
407
+ getLoops,
408
+ getActiveLoop,
409
+ getLoop,
410
+ createLoop,
411
+ updateLoop,
412
+ deleteLoop,
413
+ startLoop,
414
+ pauseLoop,
415
+ resumeLoop,
416
+ cancelLoop,
417
+ approveLoop,
418
+ completeLoop,
419
+ getLoopHistory,
420
+ getLoopConfig,
421
+ updateLoopConfig,
422
+ saveClarifications,
423
+ savePlan,
424
+ recordIteration,
425
+ getLoopHookStatus,
426
+ installLoopHooks,
427
+ };
@@ -157,6 +157,32 @@ function removeProject(manager, projectId, setProjectDir) {
157
157
  return { success: true, removed };
158
158
  }
159
159
 
160
+ /**
161
+ * Update a project's name
162
+ */
163
+ function updateProject(manager, projectId, updates) {
164
+ if (!manager) return { error: 'Manager not available' };
165
+
166
+ const registry = manager.loadProjectsRegistry();
167
+ const project = registry.projects.find(p => p.id === projectId);
168
+
169
+ if (!project) {
170
+ return { error: 'Project not found' };
171
+ }
172
+
173
+ // Only allow updating the name for now
174
+ if (updates.name && typeof updates.name === 'string' && updates.name.trim()) {
175
+ project.name = updates.name.trim();
176
+ }
177
+
178
+ manager.saveProjectsRegistry(registry);
179
+
180
+ return {
181
+ success: true,
182
+ project
183
+ };
184
+ }
185
+
160
186
  /**
161
187
  * Set active project and switch server context
162
188
  */
@@ -193,6 +219,7 @@ module.exports = {
193
219
  getProjects,
194
220
  getActiveProject,
195
221
  addProject,
222
+ updateProject,
196
223
  removeProject,
197
224
  setActiveProject,
198
225
  };
package/ui/server.cjs CHANGED
@@ -666,6 +666,33 @@ class ConfigUIServer {
666
666
  if (req.method === 'POST') return this.json(res, routes.workstreams.installWorkstreamHook());
667
667
  break;
668
668
 
669
+ // Loops (Ralph Loop)
670
+ case '/api/loops':
671
+ if (req.method === 'GET') return this.json(res, routes.loops.getLoops(this.manager));
672
+ if (req.method === 'POST') return this.json(res, routes.loops.createLoop(this.manager, body));
673
+ break;
674
+
675
+ case '/api/loops/active':
676
+ if (req.method === 'GET') return this.json(res, routes.loops.getActiveLoop(this.manager));
677
+ break;
678
+
679
+ case '/api/loops/history':
680
+ if (req.method === 'GET') return this.json(res, routes.loops.getLoopHistory(this.manager));
681
+ break;
682
+
683
+ case '/api/loops/config':
684
+ if (req.method === 'GET') return this.json(res, routes.loops.getLoopConfig(this.manager));
685
+ if (req.method === 'PUT') return this.json(res, routes.loops.updateLoopConfig(this.manager, body));
686
+ break;
687
+
688
+ case '/api/loops/hook-status':
689
+ if (req.method === 'GET') return this.json(res, routes.loops.getLoopHookStatus());
690
+ break;
691
+
692
+ case '/api/loops/install-hooks':
693
+ if (req.method === 'POST') return this.json(res, routes.loops.installLoopHooks(this.manager));
694
+ break;
695
+
669
696
  case '/api/activity':
670
697
  if (req.method === 'GET') return this.json(res, routes.activity.getActivitySummary(this.manager));
671
698
  if (req.method === 'DELETE') return this.json(res, routes.activity.clearActivity(this.manager, body.olderThanDays || 30));
@@ -725,6 +752,35 @@ class ConfigUIServer {
725
752
  }
726
753
  }
727
754
 
755
+ if (pathname.startsWith('/api/projects/') && req.method === 'PUT') {
756
+ const projectId = pathname.split('/').pop();
757
+ if (projectId && projectId !== 'active') {
758
+ return this.json(res, routes.projects.updateProject(this.manager, projectId, body));
759
+ }
760
+ }
761
+
762
+ // Dynamic loops routes
763
+ if (pathname.startsWith('/api/loops/') && !pathname.includes('/active') && !pathname.includes('/history') && !pathname.includes('/config') && !pathname.includes('/hook')) {
764
+ const parts = pathname.split('/');
765
+ const loopId = parts[3];
766
+ const action = parts[4];
767
+
768
+ if (loopId) {
769
+ if (req.method === 'GET' && !action) return this.json(res, routes.loops.getLoop(this.manager, loopId));
770
+ if (req.method === 'PUT' && !action) return this.json(res, routes.loops.updateLoop(this.manager, loopId, body));
771
+ if (req.method === 'DELETE' && !action) return this.json(res, routes.loops.deleteLoop(this.manager, loopId));
772
+ if (req.method === 'POST' && action === 'start') return this.json(res, routes.loops.startLoop(this.manager, loopId));
773
+ if (req.method === 'POST' && action === 'pause') return this.json(res, routes.loops.pauseLoop(this.manager, loopId));
774
+ if (req.method === 'POST' && action === 'resume') return this.json(res, routes.loops.resumeLoop(this.manager, loopId));
775
+ if (req.method === 'POST' && action === 'cancel') return this.json(res, routes.loops.cancelLoop(this.manager, loopId));
776
+ if (req.method === 'POST' && action === 'approve') return this.json(res, routes.loops.approveLoop(this.manager, loopId));
777
+ if (req.method === 'POST' && action === 'complete') return this.json(res, routes.loops.completeLoop(this.manager, loopId));
778
+ if (req.method === 'POST' && action === 'clarifications') return this.json(res, routes.loops.saveClarifications(this.manager, loopId, body.content));
779
+ if (req.method === 'POST' && action === 'plan') return this.json(res, routes.loops.savePlan(this.manager, loopId, body.content));
780
+ if (req.method === 'POST' && action === 'iteration') return this.json(res, routes.loops.recordIteration(this.manager, loopId, body));
781
+ }
782
+ }
783
+
728
784
  res.writeHead(404);
729
785
  res.end(JSON.stringify({ error: 'Not found' }));
730
786
  }