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/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
+ };