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.
- package/README.md +42 -0
- package/config-loader.js +32 -0
- package/hooks/ralph-loop-preprompt.sh +96 -0
- package/hooks/ralph-loop-stop.sh +101 -0
- package/lib/cli.js +81 -0
- package/lib/constants.js +1 -1
- package/lib/loops.js +849 -0
- package/package.json +1 -1
- package/ui/dist/assets/index-BX3EJoIY.css +32 -0
- package/ui/dist/assets/index-CGdqBV9k.js +3839 -0
- package/ui/dist/index.html +2 -2
- package/ui/routes/index.js +2 -0
- package/ui/routes/loops.js +427 -0
- package/ui/routes/projects.js +27 -0
- package/ui/server.cjs +56 -0
- package/ui/dist/assets/index-DZrd_FEC.js +0 -3204
- package/ui/dist/assets/index-DjLdm3Mr.css +0 -32
package/lib/loops.js
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ralph Loop feature - Autonomous development loop management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get loops directory path
|
|
10
|
+
*/
|
|
11
|
+
function getLoopsPath(installDir) {
|
|
12
|
+
return path.join(installDir, 'loops');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get loops registry file path
|
|
17
|
+
*/
|
|
18
|
+
function getLoopsRegistryPath(installDir) {
|
|
19
|
+
return path.join(getLoopsPath(installDir), 'loops.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get loops history file path
|
|
24
|
+
*/
|
|
25
|
+
function getLoopsHistoryPath(installDir) {
|
|
26
|
+
return path.join(getLoopsPath(installDir), 'history.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get loop directory for a specific loop
|
|
31
|
+
*/
|
|
32
|
+
function getLoopDir(installDir, loopId) {
|
|
33
|
+
return path.join(getLoopsPath(installDir), loopId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ensure loops directory structure exists
|
|
38
|
+
*/
|
|
39
|
+
function ensureLoopsDir(installDir) {
|
|
40
|
+
const loopsDir = getLoopsPath(installDir);
|
|
41
|
+
if (!fs.existsSync(loopsDir)) {
|
|
42
|
+
fs.mkdirSync(loopsDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
return loopsDir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load loops registry
|
|
49
|
+
*/
|
|
50
|
+
function loadLoops(installDir) {
|
|
51
|
+
const registryPath = getLoopsRegistryPath(installDir);
|
|
52
|
+
if (fs.existsSync(registryPath)) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return { loops: [], activeId: null, config: getDefaultConfig() };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { loops: [], activeId: null, config: getDefaultConfig() };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Save loops registry
|
|
64
|
+
*/
|
|
65
|
+
function saveLoops(installDir, data) {
|
|
66
|
+
ensureLoopsDir(installDir);
|
|
67
|
+
const registryPath = getLoopsRegistryPath(installDir);
|
|
68
|
+
fs.writeFileSync(registryPath, JSON.stringify(data, null, 2) + '\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load loop state from individual loop directory
|
|
73
|
+
*/
|
|
74
|
+
function loadLoopState(installDir, loopId) {
|
|
75
|
+
const stateFile = path.join(getLoopDir(installDir, loopId), 'state.json');
|
|
76
|
+
if (fs.existsSync(stateFile)) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Save loop state to individual loop directory
|
|
88
|
+
*/
|
|
89
|
+
function saveLoopState(installDir, loopId, state) {
|
|
90
|
+
const loopDir = getLoopDir(installDir, loopId);
|
|
91
|
+
if (!fs.existsSync(loopDir)) {
|
|
92
|
+
fs.mkdirSync(loopDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
const stateFile = path.join(loopDir, 'state.json');
|
|
95
|
+
state.updatedAt = new Date().toISOString();
|
|
96
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2) + '\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Load loops history
|
|
101
|
+
*/
|
|
102
|
+
function loadHistory(installDir) {
|
|
103
|
+
const historyPath = getLoopsHistoryPath(installDir);
|
|
104
|
+
if (fs.existsSync(historyPath)) {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(fs.readFileSync(historyPath, 'utf8'));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { completed: [] };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { completed: [] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Save loops history
|
|
116
|
+
*/
|
|
117
|
+
function saveHistory(installDir, data) {
|
|
118
|
+
ensureLoopsDir(installDir);
|
|
119
|
+
const historyPath = getLoopsHistoryPath(installDir);
|
|
120
|
+
fs.writeFileSync(historyPath, JSON.stringify(data, null, 2) + '\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get default loop configuration
|
|
125
|
+
*/
|
|
126
|
+
function getDefaultConfig() {
|
|
127
|
+
return {
|
|
128
|
+
maxIterations: 50,
|
|
129
|
+
maxCost: 10.00,
|
|
130
|
+
autoApprovePlan: false,
|
|
131
|
+
maxClarifyIterations: 5
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate unique loop ID
|
|
137
|
+
*/
|
|
138
|
+
function generateLoopId() {
|
|
139
|
+
return 'loop_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a new loop state object
|
|
144
|
+
*/
|
|
145
|
+
function createLoopState(name, task, options = {}) {
|
|
146
|
+
const config = options.config || getDefaultConfig();
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
id: generateLoopId(),
|
|
150
|
+
name: name,
|
|
151
|
+
workstreamId: options.workstreamId || null,
|
|
152
|
+
projectPath: options.projectPath || process.cwd(),
|
|
153
|
+
phase: 'clarify',
|
|
154
|
+
status: 'pending',
|
|
155
|
+
task: {
|
|
156
|
+
original: task,
|
|
157
|
+
clarified: null,
|
|
158
|
+
plan: null
|
|
159
|
+
},
|
|
160
|
+
iterations: {
|
|
161
|
+
current: 0,
|
|
162
|
+
max: config.maxIterations,
|
|
163
|
+
history: []
|
|
164
|
+
},
|
|
165
|
+
budget: {
|
|
166
|
+
maxIterations: config.maxIterations,
|
|
167
|
+
maxCost: config.maxCost,
|
|
168
|
+
currentCost: 0
|
|
169
|
+
},
|
|
170
|
+
taskComplete: false,
|
|
171
|
+
createdAt: new Date().toISOString(),
|
|
172
|
+
updatedAt: new Date().toISOString(),
|
|
173
|
+
completedAt: null
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* List all loops
|
|
179
|
+
*/
|
|
180
|
+
function loopList(installDir) {
|
|
181
|
+
const data = loadLoops(installDir);
|
|
182
|
+
|
|
183
|
+
// Enrich with state data
|
|
184
|
+
const enrichedLoops = data.loops.map(loop => {
|
|
185
|
+
const state = loadLoopState(installDir, loop.id);
|
|
186
|
+
return state || loop;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (enrichedLoops.length === 0) {
|
|
190
|
+
console.log('\nNo loops defined.');
|
|
191
|
+
console.log('Create one with: coder-config loop create "Task description"\n');
|
|
192
|
+
return enrichedLoops;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('\n🔄 Loops:\n');
|
|
196
|
+
for (const loop of enrichedLoops) {
|
|
197
|
+
const statusIcon = getStatusIcon(loop.status);
|
|
198
|
+
const phaseLabel = loop.phase ? `[${loop.phase}]` : '';
|
|
199
|
+
const iterLabel = loop.iterations ? `${loop.iterations.current}/${loop.iterations.max}` : '';
|
|
200
|
+
|
|
201
|
+
console.log(`${statusIcon} ${loop.name} ${phaseLabel} ${iterLabel}`);
|
|
202
|
+
console.log(` Task: ${(loop.task?.original || '').substring(0, 60)}${(loop.task?.original || '').length > 60 ? '...' : ''}`);
|
|
203
|
+
if (loop.budget?.currentCost > 0) {
|
|
204
|
+
console.log(` Cost: $${loop.budget.currentCost.toFixed(2)}/$${loop.budget.maxCost.toFixed(2)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
console.log('');
|
|
208
|
+
return enrichedLoops;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get status icon
|
|
213
|
+
*/
|
|
214
|
+
function getStatusIcon(status) {
|
|
215
|
+
const icons = {
|
|
216
|
+
pending: '○',
|
|
217
|
+
running: '●',
|
|
218
|
+
paused: '◐',
|
|
219
|
+
completed: '✓',
|
|
220
|
+
failed: '✗',
|
|
221
|
+
cancelled: '⊘'
|
|
222
|
+
};
|
|
223
|
+
return icons[status] || '○';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create a new loop
|
|
228
|
+
*/
|
|
229
|
+
function loopCreate(installDir, taskOrName, options = {}) {
|
|
230
|
+
if (!taskOrName) {
|
|
231
|
+
console.error('Usage: coder-config loop create "Task description"');
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const data = loadLoops(installDir);
|
|
236
|
+
const config = { ...getDefaultConfig(), ...data.config };
|
|
237
|
+
|
|
238
|
+
// Use task as name if no separate name provided
|
|
239
|
+
const name = options.name || taskOrName.substring(0, 50);
|
|
240
|
+
const task = taskOrName;
|
|
241
|
+
|
|
242
|
+
const state = createLoopState(name, task, { ...options, config });
|
|
243
|
+
|
|
244
|
+
// Add to registry
|
|
245
|
+
data.loops.push({
|
|
246
|
+
id: state.id,
|
|
247
|
+
name: state.name,
|
|
248
|
+
createdAt: state.createdAt
|
|
249
|
+
});
|
|
250
|
+
saveLoops(installDir, data);
|
|
251
|
+
|
|
252
|
+
// Save state file
|
|
253
|
+
const loopDir = getLoopDir(installDir, state.id);
|
|
254
|
+
fs.mkdirSync(loopDir, { recursive: true });
|
|
255
|
+
fs.mkdirSync(path.join(loopDir, 'iterations'), { recursive: true });
|
|
256
|
+
saveLoopState(installDir, state.id, state);
|
|
257
|
+
|
|
258
|
+
console.log(`✓ Created loop: ${state.name}`);
|
|
259
|
+
console.log(` ID: ${state.id}`);
|
|
260
|
+
console.log(` Start with: coder-config loop start ${state.id}`);
|
|
261
|
+
|
|
262
|
+
return state;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get a loop by ID or name
|
|
267
|
+
*/
|
|
268
|
+
function loopGet(installDir, idOrName) {
|
|
269
|
+
const data = loadLoops(installDir);
|
|
270
|
+
const entry = data.loops.find(
|
|
271
|
+
l => l.id === idOrName || l.name.toLowerCase() === idOrName.toLowerCase()
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (!entry) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return loadLoopState(installDir, entry.id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Update a loop
|
|
283
|
+
*/
|
|
284
|
+
function loopUpdate(installDir, idOrName, updates) {
|
|
285
|
+
const data = loadLoops(installDir);
|
|
286
|
+
const entry = data.loops.find(
|
|
287
|
+
l => l.id === idOrName || l.name.toLowerCase() === idOrName.toLowerCase()
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (!entry) {
|
|
291
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const state = loadLoopState(installDir, entry.id);
|
|
296
|
+
if (!state) {
|
|
297
|
+
console.error(`Loop state not found: ${idOrName}`);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Apply updates
|
|
302
|
+
if (updates.name !== undefined) {
|
|
303
|
+
state.name = updates.name;
|
|
304
|
+
entry.name = updates.name;
|
|
305
|
+
}
|
|
306
|
+
if (updates.status !== undefined) state.status = updates.status;
|
|
307
|
+
if (updates.phase !== undefined) state.phase = updates.phase;
|
|
308
|
+
if (updates.taskComplete !== undefined) state.taskComplete = updates.taskComplete;
|
|
309
|
+
if (updates.completedAt !== undefined) state.completedAt = updates.completedAt;
|
|
310
|
+
if (updates.pauseReason !== undefined) state.pauseReason = updates.pauseReason;
|
|
311
|
+
|
|
312
|
+
// Update task fields
|
|
313
|
+
if (updates.task) {
|
|
314
|
+
state.task = { ...state.task, ...updates.task };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Update budget
|
|
318
|
+
if (updates.budget) {
|
|
319
|
+
state.budget = { ...state.budget, ...updates.budget };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Update iterations
|
|
323
|
+
if (updates.iterations) {
|
|
324
|
+
state.iterations = { ...state.iterations, ...updates.iterations };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
saveLoopState(installDir, entry.id, state);
|
|
328
|
+
saveLoops(installDir, data);
|
|
329
|
+
|
|
330
|
+
return state;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Delete a loop
|
|
335
|
+
*/
|
|
336
|
+
function loopDelete(installDir, idOrName) {
|
|
337
|
+
const data = loadLoops(installDir);
|
|
338
|
+
const idx = data.loops.findIndex(
|
|
339
|
+
l => l.id === idOrName || l.name.toLowerCase() === idOrName.toLowerCase()
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (idx === -1) {
|
|
343
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const removed = data.loops.splice(idx, 1)[0];
|
|
348
|
+
|
|
349
|
+
// Remove loop directory
|
|
350
|
+
const loopDir = getLoopDir(installDir, removed.id);
|
|
351
|
+
if (fs.existsSync(loopDir)) {
|
|
352
|
+
fs.rmSync(loopDir, { recursive: true });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (data.activeId === removed.id) {
|
|
356
|
+
data.activeId = null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
saveLoops(installDir, data);
|
|
360
|
+
console.log(`✓ Deleted loop: ${removed.name}`);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Start or resume a loop
|
|
366
|
+
*/
|
|
367
|
+
function loopStart(installDir, idOrName) {
|
|
368
|
+
const state = loopGet(installDir, idOrName);
|
|
369
|
+
|
|
370
|
+
if (!state) {
|
|
371
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (state.status === 'completed') {
|
|
376
|
+
console.error('Loop is already completed. Create a new loop to restart.');
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (state.status === 'running') {
|
|
381
|
+
console.log('Loop is already running.');
|
|
382
|
+
return state;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
state.status = 'running';
|
|
386
|
+
delete state.pauseReason;
|
|
387
|
+
saveLoopState(installDir, state.id, state);
|
|
388
|
+
|
|
389
|
+
// Set as active loop
|
|
390
|
+
const data = loadLoops(installDir);
|
|
391
|
+
data.activeId = state.id;
|
|
392
|
+
saveLoops(installDir, data);
|
|
393
|
+
|
|
394
|
+
console.log(`✓ Started loop: ${state.name}`);
|
|
395
|
+
console.log(` Phase: ${state.phase}`);
|
|
396
|
+
console.log(` Iteration: ${state.iterations.current}/${state.iterations.max}`);
|
|
397
|
+
|
|
398
|
+
// Output environment setup instructions
|
|
399
|
+
console.log('\nTo run this loop with Claude Code:');
|
|
400
|
+
console.log(` export CODER_LOOP_ID=${state.id}`);
|
|
401
|
+
console.log(` claude --continue "${state.task.original}"`);
|
|
402
|
+
|
|
403
|
+
return state;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Pause a loop
|
|
408
|
+
*/
|
|
409
|
+
function loopPause(installDir, idOrName) {
|
|
410
|
+
const state = loopGet(installDir, idOrName);
|
|
411
|
+
|
|
412
|
+
if (!state) {
|
|
413
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (state.status !== 'running') {
|
|
418
|
+
console.log(`Loop is not running (status: ${state.status})`);
|
|
419
|
+
return state;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
state.status = 'paused';
|
|
423
|
+
state.pauseReason = 'user_requested';
|
|
424
|
+
saveLoopState(installDir, state.id, state);
|
|
425
|
+
|
|
426
|
+
console.log(`✓ Paused loop: ${state.name}`);
|
|
427
|
+
return state;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Resume a paused loop
|
|
432
|
+
*/
|
|
433
|
+
function loopResume(installDir, idOrName) {
|
|
434
|
+
return loopStart(installDir, idOrName);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Cancel a loop
|
|
439
|
+
*/
|
|
440
|
+
function loopCancel(installDir, idOrName) {
|
|
441
|
+
const state = loopGet(installDir, idOrName);
|
|
442
|
+
|
|
443
|
+
if (!state) {
|
|
444
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (state.status === 'completed' || state.status === 'cancelled') {
|
|
449
|
+
console.log(`Loop is already ${state.status}`);
|
|
450
|
+
return state;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
state.status = 'cancelled';
|
|
454
|
+
state.completedAt = new Date().toISOString();
|
|
455
|
+
saveLoopState(installDir, state.id, state);
|
|
456
|
+
|
|
457
|
+
// Clear active if this was it
|
|
458
|
+
const data = loadLoops(installDir);
|
|
459
|
+
if (data.activeId === state.id) {
|
|
460
|
+
data.activeId = null;
|
|
461
|
+
saveLoops(installDir, data);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(`✓ Cancelled loop: ${state.name}`);
|
|
465
|
+
return state;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Approve plan for a loop (phase 2)
|
|
470
|
+
*/
|
|
471
|
+
function loopApprove(installDir, idOrName) {
|
|
472
|
+
const state = loopGet(installDir, idOrName);
|
|
473
|
+
|
|
474
|
+
if (!state) {
|
|
475
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (state.phase !== 'plan') {
|
|
480
|
+
console.error(`Loop is not in plan phase (current: ${state.phase})`);
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
state.phase = 'execute';
|
|
485
|
+
saveLoopState(installDir, state.id, state);
|
|
486
|
+
|
|
487
|
+
console.log(`✓ Approved plan for loop: ${state.name}`);
|
|
488
|
+
console.log(' Phase advanced to: execute');
|
|
489
|
+
return state;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Get loop status (for CLI display)
|
|
494
|
+
*/
|
|
495
|
+
function loopStatus(installDir, idOrName) {
|
|
496
|
+
if (idOrName) {
|
|
497
|
+
const state = loopGet(installDir, idOrName);
|
|
498
|
+
if (!state) {
|
|
499
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
displayLoopStatus(installDir, state);
|
|
503
|
+
return state;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Show active loop status
|
|
507
|
+
const data = loadLoops(installDir);
|
|
508
|
+
if (!data.activeId) {
|
|
509
|
+
console.log('No active loop.');
|
|
510
|
+
console.log('Start a loop with: coder-config loop start <id>');
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const state = loadLoopState(installDir, data.activeId);
|
|
515
|
+
if (!state) {
|
|
516
|
+
console.log('Active loop state not found.');
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
displayLoopStatus(installDir, state);
|
|
521
|
+
return state;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Display detailed loop status
|
|
526
|
+
*/
|
|
527
|
+
function displayLoopStatus(installDir, state) {
|
|
528
|
+
console.log(`\n🔄 Loop: ${state.name}`);
|
|
529
|
+
console.log(` ID: ${state.id}`);
|
|
530
|
+
console.log(` Status: ${state.status}${state.pauseReason ? ` (${state.pauseReason})` : ''}`);
|
|
531
|
+
console.log(` Phase: ${state.phase}`);
|
|
532
|
+
console.log(` Iteration: ${state.iterations.current}/${state.iterations.max}`);
|
|
533
|
+
console.log(` Cost: $${state.budget.currentCost.toFixed(2)}/$${state.budget.maxCost.toFixed(2)}`);
|
|
534
|
+
console.log(` Task: ${state.task.original}`);
|
|
535
|
+
|
|
536
|
+
if (state.task.clarified) {
|
|
537
|
+
console.log(` Clarified: ${state.task.clarified}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Check for plan file
|
|
541
|
+
const planPath = path.join(getLoopDir(installDir, state.id), 'plan.md');
|
|
542
|
+
if (fs.existsSync(planPath)) {
|
|
543
|
+
console.log(` Plan: ${planPath}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
console.log(` Created: ${state.createdAt}`);
|
|
547
|
+
if (state.completedAt) {
|
|
548
|
+
console.log(` Completed: ${state.completedAt}`);
|
|
549
|
+
}
|
|
550
|
+
console.log('');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Show completed loops history
|
|
555
|
+
*/
|
|
556
|
+
function loopHistory(installDir) {
|
|
557
|
+
const history = loadHistory(installDir);
|
|
558
|
+
|
|
559
|
+
if (history.completed.length === 0) {
|
|
560
|
+
console.log('\nNo completed loops in history.\n');
|
|
561
|
+
return history.completed;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
console.log('\n📜 Loop History:\n');
|
|
565
|
+
for (const entry of history.completed.slice(-20).reverse()) {
|
|
566
|
+
const statusIcon = getStatusIcon(entry.status);
|
|
567
|
+
console.log(`${statusIcon} ${entry.name}`);
|
|
568
|
+
console.log(` Completed: ${entry.completedAt}`);
|
|
569
|
+
console.log(` Iterations: ${entry.totalIterations}`);
|
|
570
|
+
console.log(` Cost: $${entry.totalCost?.toFixed(2) || '0.00'}`);
|
|
571
|
+
}
|
|
572
|
+
console.log('');
|
|
573
|
+
return history.completed;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Archive a completed/cancelled loop to history
|
|
578
|
+
*/
|
|
579
|
+
function archiveLoop(installDir, loopId) {
|
|
580
|
+
const state = loadLoopState(installDir, loopId);
|
|
581
|
+
if (!state) return;
|
|
582
|
+
|
|
583
|
+
const history = loadHistory(installDir);
|
|
584
|
+
history.completed.push({
|
|
585
|
+
id: state.id,
|
|
586
|
+
name: state.name,
|
|
587
|
+
task: state.task.original,
|
|
588
|
+
status: state.status,
|
|
589
|
+
totalIterations: state.iterations.current,
|
|
590
|
+
totalCost: state.budget.currentCost,
|
|
591
|
+
createdAt: state.createdAt,
|
|
592
|
+
completedAt: state.completedAt || new Date().toISOString()
|
|
593
|
+
});
|
|
594
|
+
saveHistory(installDir, history);
|
|
595
|
+
|
|
596
|
+
// Remove from active loops
|
|
597
|
+
const data = loadLoops(installDir);
|
|
598
|
+
const idx = data.loops.findIndex(l => l.id === loopId);
|
|
599
|
+
if (idx !== -1) {
|
|
600
|
+
data.loops.splice(idx, 1);
|
|
601
|
+
if (data.activeId === loopId) {
|
|
602
|
+
data.activeId = null;
|
|
603
|
+
}
|
|
604
|
+
saveLoops(installDir, data);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Get/set loop configuration
|
|
610
|
+
*/
|
|
611
|
+
function loopConfig(installDir, updates = null) {
|
|
612
|
+
const data = loadLoops(installDir);
|
|
613
|
+
data.config = data.config || getDefaultConfig();
|
|
614
|
+
|
|
615
|
+
if (!updates) {
|
|
616
|
+
console.log('\n⚙️ Loop Configuration:\n');
|
|
617
|
+
console.log(` Max Iterations: ${data.config.maxIterations}`);
|
|
618
|
+
console.log(` Max Cost: $${data.config.maxCost.toFixed(2)}`);
|
|
619
|
+
console.log(` Auto-approve Plan: ${data.config.autoApprovePlan}`);
|
|
620
|
+
console.log(` Max Clarify Iterations: ${data.config.maxClarifyIterations}`);
|
|
621
|
+
console.log('');
|
|
622
|
+
return data.config;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Apply updates
|
|
626
|
+
if (updates.maxIterations !== undefined) {
|
|
627
|
+
data.config.maxIterations = parseInt(updates.maxIterations, 10);
|
|
628
|
+
}
|
|
629
|
+
if (updates.maxCost !== undefined) {
|
|
630
|
+
data.config.maxCost = parseFloat(updates.maxCost);
|
|
631
|
+
}
|
|
632
|
+
if (updates.autoApprovePlan !== undefined) {
|
|
633
|
+
data.config.autoApprovePlan = updates.autoApprovePlan === true || updates.autoApprovePlan === 'true';
|
|
634
|
+
}
|
|
635
|
+
if (updates.maxClarifyIterations !== undefined) {
|
|
636
|
+
data.config.maxClarifyIterations = parseInt(updates.maxClarifyIterations, 10);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
saveLoops(installDir, data);
|
|
640
|
+
console.log('✓ Configuration updated');
|
|
641
|
+
return data.config;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Get active loop
|
|
646
|
+
*/
|
|
647
|
+
function getActiveLoop(installDir) {
|
|
648
|
+
// Check env var first
|
|
649
|
+
const envLoopId = process.env.CODER_LOOP_ID;
|
|
650
|
+
if (envLoopId) {
|
|
651
|
+
const state = loadLoopState(installDir, envLoopId);
|
|
652
|
+
if (state) return state;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Fall back to registry activeId
|
|
656
|
+
const data = loadLoops(installDir);
|
|
657
|
+
if (data.activeId) {
|
|
658
|
+
return loadLoopState(installDir, data.activeId);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Record an iteration
|
|
666
|
+
*/
|
|
667
|
+
function recordIteration(installDir, loopId, iteration) {
|
|
668
|
+
const state = loadLoopState(installDir, loopId);
|
|
669
|
+
if (!state) return null;
|
|
670
|
+
|
|
671
|
+
state.iterations.history.push(iteration);
|
|
672
|
+
state.iterations.current = iteration.n;
|
|
673
|
+
|
|
674
|
+
// Update cost
|
|
675
|
+
if (iteration.cost) {
|
|
676
|
+
state.budget.currentCost += iteration.cost;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Save iteration file
|
|
680
|
+
const iterDir = path.join(getLoopDir(installDir, loopId), 'iterations');
|
|
681
|
+
if (!fs.existsSync(iterDir)) {
|
|
682
|
+
fs.mkdirSync(iterDir, { recursive: true });
|
|
683
|
+
}
|
|
684
|
+
fs.writeFileSync(
|
|
685
|
+
path.join(iterDir, `${iteration.n}.json`),
|
|
686
|
+
JSON.stringify(iteration, null, 2) + '\n'
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
saveLoopState(installDir, loopId, state);
|
|
690
|
+
return state;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Save clarifications to file
|
|
695
|
+
*/
|
|
696
|
+
function saveClarifications(installDir, loopId, content) {
|
|
697
|
+
const loopDir = getLoopDir(installDir, loopId);
|
|
698
|
+
const clarifyPath = path.join(loopDir, 'clarifications.md');
|
|
699
|
+
fs.writeFileSync(clarifyPath, content);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Save plan to file
|
|
704
|
+
*/
|
|
705
|
+
function savePlan(installDir, loopId, content) {
|
|
706
|
+
const loopDir = getLoopDir(installDir, loopId);
|
|
707
|
+
const planPath = path.join(loopDir, 'plan.md');
|
|
708
|
+
fs.writeFileSync(planPath, content);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Load clarifications from file
|
|
713
|
+
*/
|
|
714
|
+
function loadClarifications(installDir, loopId) {
|
|
715
|
+
const clarifyPath = path.join(getLoopDir(installDir, loopId), 'clarifications.md');
|
|
716
|
+
if (fs.existsSync(clarifyPath)) {
|
|
717
|
+
return fs.readFileSync(clarifyPath, 'utf8');
|
|
718
|
+
}
|
|
719
|
+
return '';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Load plan from file
|
|
724
|
+
*/
|
|
725
|
+
function loadPlan(installDir, loopId) {
|
|
726
|
+
const planPath = path.join(getLoopDir(installDir, loopId), 'plan.md');
|
|
727
|
+
if (fs.existsSync(planPath)) {
|
|
728
|
+
return fs.readFileSync(planPath, 'utf8');
|
|
729
|
+
}
|
|
730
|
+
return '';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Inject loop context (for hooks)
|
|
735
|
+
*/
|
|
736
|
+
function loopInject(installDir, silent = false) {
|
|
737
|
+
const active = getActiveLoop(installDir);
|
|
738
|
+
|
|
739
|
+
if (!active) {
|
|
740
|
+
if (!silent) console.log('No active loop');
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const lines = [];
|
|
745
|
+
lines.push('<ralph-loop-context>');
|
|
746
|
+
lines.push(`Loop: ${active.name}`);
|
|
747
|
+
lines.push(`Phase: ${active.phase}`);
|
|
748
|
+
lines.push(`Iteration: ${active.iterations.current}/${active.iterations.max}`);
|
|
749
|
+
lines.push(`Status: ${active.status}`);
|
|
750
|
+
|
|
751
|
+
const clarifications = loadClarifications(installDir, active.id);
|
|
752
|
+
if (clarifications) {
|
|
753
|
+
lines.push('');
|
|
754
|
+
lines.push('## Clarifications');
|
|
755
|
+
lines.push(clarifications);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const plan = loadPlan(installDir, active.id);
|
|
759
|
+
if (plan) {
|
|
760
|
+
lines.push('');
|
|
761
|
+
lines.push('## Plan');
|
|
762
|
+
lines.push(plan);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
lines.push('');
|
|
766
|
+
lines.push(`Task: ${active.task.original}`);
|
|
767
|
+
lines.push('</ralph-loop-context>');
|
|
768
|
+
|
|
769
|
+
const output = lines.join('\n');
|
|
770
|
+
console.log(output);
|
|
771
|
+
return output;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Mark loop as complete
|
|
776
|
+
*/
|
|
777
|
+
function loopComplete(installDir, idOrName) {
|
|
778
|
+
const state = loopGet(installDir, idOrName);
|
|
779
|
+
|
|
780
|
+
if (!state) {
|
|
781
|
+
console.error(`Loop not found: ${idOrName}`);
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
state.status = 'completed';
|
|
786
|
+
state.taskComplete = true;
|
|
787
|
+
state.completedAt = new Date().toISOString();
|
|
788
|
+
saveLoopState(installDir, state.id, state);
|
|
789
|
+
|
|
790
|
+
// Archive to history
|
|
791
|
+
archiveLoop(installDir, state.id);
|
|
792
|
+
|
|
793
|
+
console.log(`✓ Completed loop: ${state.name}`);
|
|
794
|
+
return state;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
module.exports = {
|
|
798
|
+
// Path helpers
|
|
799
|
+
getLoopsPath,
|
|
800
|
+
getLoopsRegistryPath,
|
|
801
|
+
getLoopsHistoryPath,
|
|
802
|
+
getLoopDir,
|
|
803
|
+
|
|
804
|
+
// Data operations
|
|
805
|
+
loadLoops,
|
|
806
|
+
saveLoops,
|
|
807
|
+
loadLoopState,
|
|
808
|
+
saveLoopState,
|
|
809
|
+
loadHistory,
|
|
810
|
+
saveHistory,
|
|
811
|
+
|
|
812
|
+
// CRUD operations
|
|
813
|
+
loopList,
|
|
814
|
+
loopCreate,
|
|
815
|
+
loopGet,
|
|
816
|
+
loopUpdate,
|
|
817
|
+
loopDelete,
|
|
818
|
+
|
|
819
|
+
// Lifecycle operations
|
|
820
|
+
loopStart,
|
|
821
|
+
loopPause,
|
|
822
|
+
loopResume,
|
|
823
|
+
loopCancel,
|
|
824
|
+
loopApprove,
|
|
825
|
+
loopComplete,
|
|
826
|
+
|
|
827
|
+
// Status operations
|
|
828
|
+
loopStatus,
|
|
829
|
+
loopHistory,
|
|
830
|
+
loopConfig,
|
|
831
|
+
getActiveLoop,
|
|
832
|
+
|
|
833
|
+
// Iteration tracking
|
|
834
|
+
recordIteration,
|
|
835
|
+
|
|
836
|
+
// File operations
|
|
837
|
+
saveClarifications,
|
|
838
|
+
savePlan,
|
|
839
|
+
loadClarifications,
|
|
840
|
+
loadPlan,
|
|
841
|
+
|
|
842
|
+
// Hook support
|
|
843
|
+
loopInject,
|
|
844
|
+
archiveLoop,
|
|
845
|
+
|
|
846
|
+
// Utilities
|
|
847
|
+
getDefaultConfig,
|
|
848
|
+
generateLoopId,
|
|
849
|
+
};
|