agileflow 2.89.2 → 2.90.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/content-sanitizer.js +463 -0
  4. package/lib/error-codes.js +544 -0
  5. package/lib/errors.js +336 -5
  6. package/lib/feedback.js +561 -0
  7. package/lib/path-resolver.js +396 -0
  8. package/lib/placeholder-registry.js +617 -0
  9. package/lib/session-registry.js +461 -0
  10. package/lib/smart-json-file.js +653 -0
  11. package/lib/table-formatter.js +504 -0
  12. package/lib/transient-status.js +374 -0
  13. package/lib/ui-manager.js +612 -0
  14. package/lib/validate-args.js +213 -0
  15. package/lib/validate-names.js +143 -0
  16. package/lib/validate-paths.js +434 -0
  17. package/lib/validate.js +38 -584
  18. package/package.json +4 -1
  19. package/scripts/agileflow-configure.js +40 -1440
  20. package/scripts/agileflow-welcome.js +2 -1
  21. package/scripts/check-update.js +16 -3
  22. package/scripts/lib/configure-detect.js +383 -0
  23. package/scripts/lib/configure-features.js +811 -0
  24. package/scripts/lib/configure-repair.js +314 -0
  25. package/scripts/lib/configure-utils.js +115 -0
  26. package/scripts/lib/frontmatter-parser.js +3 -3
  27. package/scripts/lib/sessionRegistry.js +682 -0
  28. package/scripts/obtain-context.js +417 -113
  29. package/scripts/ralph-loop.js +1 -1
  30. package/scripts/session-manager.js +77 -10
  31. package/scripts/tui/App.js +176 -0
  32. package/scripts/tui/index.js +75 -0
  33. package/scripts/tui/lib/crashRecovery.js +302 -0
  34. package/scripts/tui/lib/eventStream.js +316 -0
  35. package/scripts/tui/lib/keyboard.js +252 -0
  36. package/scripts/tui/lib/loopControl.js +371 -0
  37. package/scripts/tui/panels/OutputPanel.js +278 -0
  38. package/scripts/tui/panels/SessionPanel.js +178 -0
  39. package/scripts/tui/panels/TracePanel.js +333 -0
  40. package/src/core/commands/tui.md +91 -0
  41. package/tools/cli/commands/config.js +10 -33
  42. package/tools/cli/commands/doctor.js +48 -40
  43. package/tools/cli/commands/list.js +49 -37
  44. package/tools/cli/commands/status.js +13 -37
  45. package/tools/cli/commands/uninstall.js +12 -41
  46. package/tools/cli/installers/core/installer.js +75 -12
  47. package/tools/cli/installers/ide/_interface.js +238 -0
  48. package/tools/cli/installers/ide/codex.js +2 -2
  49. package/tools/cli/installers/ide/manager.js +15 -0
  50. package/tools/cli/lib/command-context.js +374 -0
  51. package/tools/cli/lib/config-manager.js +394 -0
  52. package/tools/cli/lib/content-injector.js +69 -16
  53. package/tools/cli/lib/ide-errors.js +163 -29
  54. package/tools/cli/lib/ide-registry.js +186 -0
  55. package/tools/cli/lib/npm-utils.js +16 -3
  56. package/tools/cli/lib/self-update.js +148 -0
  57. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -664,7 +664,7 @@ function handleLoop(rootDir) {
664
664
  }
665
665
 
666
666
  // Evaluate discretion conditions
667
- let discretionResults = [];
667
+ const discretionResults = [];
668
668
  if (hasDiscretionConditions) {
669
669
  console.log('');
670
670
  console.log(`${c.blue}Evaluating discretion conditions...${c.reset}`);
@@ -259,6 +259,23 @@ function unregisterSession(sessionId) {
259
259
  }
260
260
  }
261
261
 
262
+ // Get session by ID
263
+ function getSession(sessionId) {
264
+ const registry = loadRegistry();
265
+ const session = registry.sessions[sessionId];
266
+ if (!session) {
267
+ return null;
268
+ }
269
+ // Ensure thread_type exists (migration for legacy sessions)
270
+ const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
271
+ return {
272
+ id: sessionId,
273
+ ...session,
274
+ thread_type: threadType,
275
+ active: isSessionActive(sessionId),
276
+ };
277
+ }
278
+
262
279
  // Create new session with worktree
263
280
  function createSession(options = {}) {
264
281
  const registry = loadRegistry();
@@ -1006,6 +1023,22 @@ function main() {
1006
1023
  break;
1007
1024
  }
1008
1025
 
1026
+ case 'get': {
1027
+ const sessionId = args[1];
1028
+ if (!sessionId) {
1029
+ console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1030
+ return;
1031
+ }
1032
+ // Use the exported getSession function for consistency
1033
+ const session = getSession(sessionId);
1034
+ if (!session) {
1035
+ console.log(JSON.stringify({ success: false, error: `Session ${sessionId} not found` }));
1036
+ return;
1037
+ }
1038
+ console.log(JSON.stringify({ success: true, ...session }));
1039
+ break;
1040
+ }
1041
+
1009
1042
  // PERFORMANCE: Combined command for welcome script (saves ~200ms from 3 subprocess calls)
1010
1043
  case 'full-status': {
1011
1044
  const nickname = args[1] || null;
@@ -1248,6 +1281,7 @@ ${c.cyan}Commands:${c.reset}
1248
1281
  count Count other active sessions
1249
1282
  delete <id> [--remove-worktree] Delete session
1250
1283
  status Get current session status
1284
+ get <id> Get specific session by ID
1251
1285
  full-status Combined register+count+status (optimized)
1252
1286
  switch <id|nickname> Switch active session context (for /add-dir)
1253
1287
  active Get currently switched session (if any)
@@ -1667,14 +1701,47 @@ function resolveConflict(resolution) {
1667
1701
  try {
1668
1702
  switch (gitStrategy) {
1669
1703
  case 'union':
1670
- // Union merge - keep both sides (works for text files)
1671
- execSync(`git checkout --ours "${file}" && git checkout --theirs "${file}" --`, {
1672
- cwd: ROOT,
1673
- encoding: 'utf8',
1674
- });
1675
- // Actually, use git merge-file for union
1676
- // For simplicity, accept theirs for now and log
1677
- execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
1704
+ // Union merge - concatenate both versions (works for additive files like docs/tests)
1705
+ // Use git merge-file with --union flag for true union merge
1706
+ // This keeps both ours and theirs changes, separated by markers only if truly conflicting
1707
+ try {
1708
+ // Get the base, ours, and theirs versions
1709
+ const base = spawnSync('git', ['show', `:1:${file}`], { cwd: ROOT, encoding: 'utf8' });
1710
+ const ours = spawnSync('git', ['show', `:2:${file}`], { cwd: ROOT, encoding: 'utf8' });
1711
+ const theirs = spawnSync('git', ['show', `:3:${file}`], { cwd: ROOT, encoding: 'utf8' });
1712
+
1713
+ // If we can get all three, use merge-file with union
1714
+ if (base.status === 0 && ours.status === 0 && theirs.status === 0) {
1715
+ // Write temp files for merge-file
1716
+ const tmpBase = path.join(ROOT, '.git', 'MERGE_BASE_TMP');
1717
+ const tmpOurs = path.join(ROOT, '.git', 'MERGE_OURS_TMP');
1718
+ const tmpTheirs = path.join(ROOT, '.git', 'MERGE_THEIRS_TMP');
1719
+
1720
+ fs.writeFileSync(tmpBase, base.stdout);
1721
+ fs.writeFileSync(tmpOurs, ours.stdout);
1722
+ fs.writeFileSync(tmpTheirs, theirs.stdout);
1723
+
1724
+ // Run merge-file with --union (keeps both sides for conflicts)
1725
+ spawnSync('git', ['merge-file', '--union', tmpOurs, tmpBase, tmpTheirs], {
1726
+ cwd: ROOT,
1727
+ encoding: 'utf8',
1728
+ });
1729
+
1730
+ // Copy merged result to working tree
1731
+ fs.copyFileSync(tmpOurs, path.join(ROOT, file));
1732
+
1733
+ // Cleanup temp files
1734
+ fs.unlinkSync(tmpBase);
1735
+ fs.unlinkSync(tmpOurs);
1736
+ fs.unlinkSync(tmpTheirs);
1737
+ } else {
1738
+ // Fallback: accept theirs for docs/tests (session's additions are more important)
1739
+ execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
1740
+ }
1741
+ } catch (unionError) {
1742
+ // Fallback to theirs on any error
1743
+ execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
1744
+ }
1678
1745
  break;
1679
1746
 
1680
1747
  case 'theirs':
@@ -1689,8 +1756,7 @@ function resolveConflict(resolution) {
1689
1756
 
1690
1757
  case 'recursive':
1691
1758
  default:
1692
- // Try to use git's recursive strategy
1693
- // For conflicts, we'll favor theirs (the session's work)
1759
+ // For source code conflicts, favor theirs (the session's work)
1694
1760
  execSync(`git checkout --theirs "${file}"`, { cwd: ROOT, encoding: 'utf8' });
1695
1761
  break;
1696
1762
  }
@@ -1936,6 +2002,7 @@ module.exports = {
1936
2002
  saveRegistry,
1937
2003
  registerSession,
1938
2004
  unregisterSession,
2005
+ getSession,
1939
2006
  createSession,
1940
2007
  getSessions,
1941
2008
  getActiveSessionCount,
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const React = require('react');
4
+ const { Box, Text, useInput, useApp } = require('ink');
5
+ const { KeyboardHandler, formatBindings, DEFAULT_BINDINGS } = require('./lib/keyboard');
6
+
7
+ /**
8
+ * Main TUI Application Component
9
+ *
10
+ * Provides the base layout and keyboard handling for AgileFlow TUI.
11
+ * Key bindings: Q=quit, S=start, P=pause, R=resume, T=trace, 1-9=sessions
12
+ */
13
+ function App({
14
+ children,
15
+ title = 'AgileFlow TUI',
16
+ showFooter = true,
17
+ onAction = null,
18
+ bindings = DEFAULT_BINDINGS
19
+ }) {
20
+ const { exit } = useApp();
21
+ const [showHelp, setShowHelp] = React.useState(false);
22
+ const [lastAction, setLastAction] = React.useState(null);
23
+
24
+ // Create keyboard handler
25
+ const keyboardRef = React.useRef(null);
26
+ if (!keyboardRef.current) {
27
+ keyboardRef.current = new KeyboardHandler({ bindings });
28
+ }
29
+
30
+ // Set up event listeners
31
+ React.useEffect(() => {
32
+ const keyboard = keyboardRef.current;
33
+
34
+ keyboard.on('quit', () => exit());
35
+ keyboard.on('help', () => setShowHelp(prev => !prev));
36
+
37
+ // Forward all actions to parent
38
+ keyboard.on('action', (action) => {
39
+ setLastAction(action);
40
+ if (onAction) {
41
+ onAction(action);
42
+ }
43
+ });
44
+
45
+ return () => {
46
+ keyboard.removeAllListeners();
47
+ };
48
+ }, [exit, onAction]);
49
+
50
+ // Handle key input
51
+ useInput((input, key) => {
52
+ keyboardRef.current.processKey(input, key);
53
+ });
54
+
55
+ // Format footer bindings
56
+ const footerBindings = formatBindings(bindings);
57
+
58
+ return React.createElement(
59
+ Box,
60
+ {
61
+ flexDirection: 'column',
62
+ width: '100%',
63
+ minHeight: 20
64
+ },
65
+ // Header
66
+ React.createElement(
67
+ Box,
68
+ {
69
+ borderStyle: 'round',
70
+ borderColor: 'cyan',
71
+ paddingX: 1,
72
+ justifyContent: 'center'
73
+ },
74
+ React.createElement(
75
+ Text,
76
+ { bold: true, color: 'cyan' },
77
+ title
78
+ ),
79
+ lastAction && React.createElement(
80
+ Text,
81
+ { dimColor: true },
82
+ ` [${lastAction.action}]`
83
+ )
84
+ ),
85
+ // Main content area
86
+ React.createElement(
87
+ Box,
88
+ {
89
+ flexDirection: 'column',
90
+ flexGrow: 1,
91
+ paddingX: 1,
92
+ paddingY: 1
93
+ },
94
+ showHelp
95
+ ? React.createElement(HelpPanel, { bindings })
96
+ : children
97
+ ),
98
+ // Footer with key bindings
99
+ showFooter && React.createElement(
100
+ Box,
101
+ {
102
+ borderStyle: 'single',
103
+ borderColor: 'gray',
104
+ paddingX: 1,
105
+ justifyContent: 'space-between'
106
+ },
107
+ React.createElement(
108
+ Box,
109
+ { flexDirection: 'row' },
110
+ footerBindings.map((binding, i) =>
111
+ React.createElement(
112
+ Text,
113
+ { key: `binding-${i}`, dimColor: true },
114
+ i > 0 ? ' | ' : '',
115
+ binding
116
+ )
117
+ )
118
+ ),
119
+ React.createElement(
120
+ Text,
121
+ { dimColor: true },
122
+ '1-9:Sessions | AgileFlow v2.89.3'
123
+ )
124
+ )
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Help Panel component
130
+ */
131
+ function HelpPanel({ bindings = DEFAULT_BINDINGS }) {
132
+ const groups = {
133
+ 'Loop Control': ['start', 'pause', 'resume'],
134
+ 'View': ['trace', 'help'],
135
+ 'Navigation': ['quit'],
136
+ 'Sessions': ['session1', 'session2', 'session3']
137
+ };
138
+
139
+ return React.createElement(
140
+ Box,
141
+ { flexDirection: 'column', padding: 1 },
142
+ React.createElement(
143
+ Text,
144
+ { bold: true, color: 'cyan' },
145
+ 'Key Bindings'
146
+ ),
147
+ React.createElement(Box, { marginTop: 1 }),
148
+ Object.entries(groups).map(([groupName, actions]) =>
149
+ React.createElement(
150
+ Box,
151
+ { key: groupName, flexDirection: 'column', marginBottom: 1 },
152
+ React.createElement(
153
+ Text,
154
+ { bold: true },
155
+ groupName + ':'
156
+ ),
157
+ actions.map(action => {
158
+ const binding = bindings[action];
159
+ if (!binding) return null;
160
+ return React.createElement(
161
+ Text,
162
+ { key: action, dimColor: true },
163
+ ` ${binding.key.toUpperCase()} - ${binding.description}`
164
+ );
165
+ })
166
+ )
167
+ ),
168
+ React.createElement(
169
+ Text,
170
+ { dimColor: true, marginTop: 1 },
171
+ 'Press ? to close help'
172
+ )
173
+ );
174
+ }
175
+
176
+ module.exports = { App, HelpPanel };
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * AgileFlow TUI - Terminal User Interface
6
+ *
7
+ * Real-time visualization for session monitoring, multi-agent orchestration,
8
+ * and interactive loop control.
9
+ *
10
+ * Usage:
11
+ * node scripts/tui/index.js
12
+ * npx agileflow tui
13
+ *
14
+ * Key bindings:
15
+ * q - Quit TUI
16
+ * s - Start loop on current story
17
+ * p - Pause active loop
18
+ * r - Resume paused loop
19
+ * t - Toggle trace panel
20
+ * 1-9 - Switch session focus
21
+ */
22
+
23
+ const React = require('react');
24
+ const { render, Box, Text } = require('ink');
25
+ const { App } = require('./App');
26
+ const { SessionPanel } = require('./panels/SessionPanel');
27
+ const { OutputPanel } = require('./panels/OutputPanel');
28
+
29
+ // Main TUI Layout - split panel view
30
+ function MainLayout() {
31
+ return React.createElement(
32
+ Box,
33
+ { flexDirection: 'row', width: '100%', minHeight: 15 },
34
+ // Left panel - Sessions (40% width)
35
+ React.createElement(
36
+ Box,
37
+ { flexDirection: 'column', width: '40%', paddingRight: 1 },
38
+ React.createElement(SessionPanel, { refreshInterval: 5000 })
39
+ ),
40
+ // Right panel - Agent Output (60% width)
41
+ React.createElement(
42
+ Box,
43
+ { flexDirection: 'column', width: '60%' },
44
+ React.createElement(OutputPanel, {
45
+ maxMessages: 100,
46
+ showTimestamp: true,
47
+ title: 'AGENT OUTPUT'
48
+ })
49
+ )
50
+ );
51
+ }
52
+
53
+ // Main entry point
54
+ function main() {
55
+ const instance = render(
56
+ React.createElement(
57
+ App,
58
+ { title: 'AgileFlow TUI' },
59
+ React.createElement(MainLayout)
60
+ )
61
+ );
62
+
63
+ // Handle clean exit
64
+ instance.waitUntilExit().then(() => {
65
+ console.log('AgileFlow TUI closed.');
66
+ process.exit(0);
67
+ });
68
+ }
69
+
70
+ // Run if executed directly
71
+ if (require.main === module) {
72
+ main();
73
+ }
74
+
75
+ module.exports = { main };
@@ -0,0 +1,302 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Crash Recovery - Session state persistence and recovery
5
+ *
6
+ * Persists ralph-loop state to enable recovery after crashes.
7
+ * Stores checkpoints on each iteration and prompts for recovery
8
+ * when incomplete state is detected.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ // Get project root
15
+ let getProjectRoot;
16
+ try {
17
+ getProjectRoot = require('../../../lib/paths').getProjectRoot;
18
+ } catch (e) {
19
+ getProjectRoot = () => process.cwd();
20
+ }
21
+
22
+ // Get safe JSON utilities
23
+ let safeReadJSON, safeWriteJSON;
24
+ try {
25
+ const errors = require('../../../lib/errors');
26
+ safeReadJSON = errors.safeReadJSON;
27
+ safeWriteJSON = errors.safeWriteJSON;
28
+ } catch (e) {
29
+ safeReadJSON = (filePath, opts = {}) => {
30
+ try {
31
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
32
+ return { ok: true, data };
33
+ } catch (e) {
34
+ return { ok: false, error: e.message, data: opts.defaultValue };
35
+ }
36
+ };
37
+ safeWriteJSON = (filePath, data) => {
38
+ const dir = path.dirname(filePath);
39
+ if (!fs.existsSync(dir)) {
40
+ fs.mkdirSync(dir, { recursive: true });
41
+ }
42
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
43
+ return { ok: true };
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Get checkpoint file path
49
+ */
50
+ function getCheckpointPath(sessionId = 'default') {
51
+ const rootDir = getProjectRoot();
52
+ return path.join(rootDir, '.agileflow', 'sessions', `${sessionId}.checkpoint`);
53
+ }
54
+
55
+ /**
56
+ * Get session state path
57
+ */
58
+ function getSessionStatePath() {
59
+ const rootDir = getProjectRoot();
60
+ return path.join(rootDir, 'docs', '09-agents', 'session-state.json');
61
+ }
62
+
63
+ /**
64
+ * Create checkpoint
65
+ * Called after each iteration to persist state
66
+ */
67
+ function createCheckpoint(sessionId = 'default', loopState = null) {
68
+ const checkpointPath = getCheckpointPath(sessionId);
69
+
70
+ // Get current loop state if not provided
71
+ if (!loopState) {
72
+ const statePath = getSessionStatePath();
73
+ const result = safeReadJSON(statePath, { defaultValue: {} });
74
+ loopState = result.ok ? result.data.ralph_loop : null;
75
+ }
76
+
77
+ if (!loopState || !loopState.enabled) {
78
+ return { ok: false, error: 'No active loop state to checkpoint' };
79
+ }
80
+
81
+ const checkpoint = {
82
+ version: 1,
83
+ session_id: sessionId,
84
+ created_at: new Date().toISOString(),
85
+ loop_state: {
86
+ epic: loopState.epic,
87
+ current_story: loopState.current_story,
88
+ iteration: loopState.iteration || 0,
89
+ max_iterations: loopState.max_iterations || 20,
90
+ visual_mode: loopState.visual_mode || false,
91
+ coverage_mode: loopState.coverage_mode || false,
92
+ coverage_threshold: loopState.coverage_threshold || 80,
93
+ coverage_current: loopState.coverage_current || 0,
94
+ started_at: loopState.started_at,
95
+ conditions: loopState.conditions || []
96
+ },
97
+ recovery_info: {
98
+ can_resume: true,
99
+ last_checkpoint: new Date().toISOString()
100
+ }
101
+ };
102
+
103
+ const result = safeWriteJSON(checkpointPath, checkpoint);
104
+
105
+ return {
106
+ ok: result.ok,
107
+ checkpoint,
108
+ path: checkpointPath
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Load checkpoint
114
+ */
115
+ function loadCheckpoint(sessionId = 'default') {
116
+ const checkpointPath = getCheckpointPath(sessionId);
117
+
118
+ if (!fs.existsSync(checkpointPath)) {
119
+ return { ok: false, exists: false, error: 'No checkpoint found' };
120
+ }
121
+
122
+ const result = safeReadJSON(checkpointPath, { defaultValue: null });
123
+
124
+ if (!result.ok || !result.data) {
125
+ return { ok: false, exists: true, error: 'Failed to read checkpoint' };
126
+ }
127
+
128
+ return {
129
+ ok: true,
130
+ exists: true,
131
+ checkpoint: result.data
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Check if recovery is needed
137
+ * Detects incomplete loop state from crash
138
+ */
139
+ function checkRecoveryNeeded(sessionId = 'default') {
140
+ // Check for checkpoint file
141
+ const checkpointResult = loadCheckpoint(sessionId);
142
+
143
+ if (!checkpointResult.exists) {
144
+ return { needed: false, reason: 'no_checkpoint' };
145
+ }
146
+
147
+ if (!checkpointResult.ok) {
148
+ return { needed: false, reason: 'checkpoint_invalid' };
149
+ }
150
+
151
+ const checkpoint = checkpointResult.checkpoint;
152
+
153
+ // Check if checkpoint is stale (older than 1 hour without update)
154
+ const lastCheckpoint = new Date(checkpoint.recovery_info?.last_checkpoint || checkpoint.created_at);
155
+ const now = new Date();
156
+ const hoursSinceCheckpoint = (now - lastCheckpoint) / (1000 * 60 * 60);
157
+
158
+ if (hoursSinceCheckpoint > 24) {
159
+ return { needed: false, reason: 'checkpoint_expired', checkpoint };
160
+ }
161
+
162
+ // Check if loop was in progress
163
+ const loopState = checkpoint.loop_state;
164
+ if (!loopState || loopState.iteration === 0) {
165
+ return { needed: false, reason: 'loop_not_started', checkpoint };
166
+ }
167
+
168
+ // Check current session state
169
+ const statePath = getSessionStatePath();
170
+ const stateResult = safeReadJSON(statePath, { defaultValue: {} });
171
+ const currentState = stateResult.ok ? stateResult.data : {};
172
+
173
+ // If loop is still enabled and matches checkpoint, no recovery needed
174
+ if (currentState.ralph_loop && currentState.ralph_loop.enabled) {
175
+ if (currentState.ralph_loop.iteration === loopState.iteration) {
176
+ return { needed: false, reason: 'loop_still_active', checkpoint };
177
+ }
178
+ }
179
+
180
+ // Loop was in progress but not active now - recovery needed
181
+ return {
182
+ needed: true,
183
+ reason: 'incomplete_loop',
184
+ checkpoint,
185
+ recovery_options: {
186
+ resume: {
187
+ iteration: loopState.iteration,
188
+ story: loopState.current_story,
189
+ epic: loopState.epic
190
+ },
191
+ fresh: {
192
+ message: 'Start fresh from beginning of epic'
193
+ }
194
+ }
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Resume from checkpoint
200
+ */
201
+ function resumeFromCheckpoint(sessionId = 'default') {
202
+ const checkpointResult = loadCheckpoint(sessionId);
203
+
204
+ if (!checkpointResult.ok || !checkpointResult.checkpoint) {
205
+ return { ok: false, error: 'No valid checkpoint to resume from' };
206
+ }
207
+
208
+ const checkpoint = checkpointResult.checkpoint;
209
+ const loopState = checkpoint.loop_state;
210
+
211
+ // Restore loop state to session state
212
+ const statePath = getSessionStatePath();
213
+ const stateResult = safeReadJSON(statePath, { defaultValue: {} });
214
+ const state = stateResult.ok ? stateResult.data : {};
215
+
216
+ state.ralph_loop = {
217
+ enabled: true,
218
+ epic: loopState.epic,
219
+ current_story: loopState.current_story,
220
+ iteration: loopState.iteration,
221
+ max_iterations: loopState.max_iterations,
222
+ visual_mode: loopState.visual_mode,
223
+ coverage_mode: loopState.coverage_mode,
224
+ coverage_threshold: loopState.coverage_threshold,
225
+ coverage_current: loopState.coverage_current,
226
+ conditions: loopState.conditions,
227
+ started_at: loopState.started_at,
228
+ resumed_at: new Date().toISOString(),
229
+ resumed_from_checkpoint: true
230
+ };
231
+
232
+ safeWriteJSON(statePath, state);
233
+
234
+ return {
235
+ ok: true,
236
+ resumed: true,
237
+ iteration: loopState.iteration,
238
+ story: loopState.current_story,
239
+ epic: loopState.epic
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Clear checkpoint (on clean completion)
245
+ */
246
+ function clearCheckpoint(sessionId = 'default') {
247
+ const checkpointPath = getCheckpointPath(sessionId);
248
+
249
+ if (fs.existsSync(checkpointPath)) {
250
+ fs.unlinkSync(checkpointPath);
251
+ return { ok: true, cleared: true };
252
+ }
253
+
254
+ return { ok: true, cleared: false };
255
+ }
256
+
257
+ /**
258
+ * Start fresh (clear checkpoint and any existing loop state)
259
+ */
260
+ function startFresh(sessionId = 'default') {
261
+ // Clear checkpoint
262
+ clearCheckpoint(sessionId);
263
+
264
+ // Clear loop state
265
+ const statePath = getSessionStatePath();
266
+ const stateResult = safeReadJSON(statePath, { defaultValue: {} });
267
+ const state = stateResult.ok ? stateResult.data : {};
268
+
269
+ if (state.ralph_loop) {
270
+ delete state.ralph_loop;
271
+ safeWriteJSON(statePath, state);
272
+ }
273
+
274
+ return { ok: true, cleared: true };
275
+ }
276
+
277
+ /**
278
+ * Get recovery status summary
279
+ */
280
+ function getRecoveryStatus(sessionId = 'default') {
281
+ const recovery = checkRecoveryNeeded(sessionId);
282
+ const checkpoint = loadCheckpoint(sessionId);
283
+
284
+ return {
285
+ recoveryNeeded: recovery.needed,
286
+ reason: recovery.reason,
287
+ hasCheckpoint: checkpoint.exists,
288
+ checkpoint: checkpoint.checkpoint,
289
+ options: recovery.recovery_options
290
+ };
291
+ }
292
+
293
+ module.exports = {
294
+ getCheckpointPath,
295
+ createCheckpoint,
296
+ loadCheckpoint,
297
+ checkRecoveryNeeded,
298
+ resumeFromCheckpoint,
299
+ clearCheckpoint,
300
+ startFresh,
301
+ getRecoveryStatus
302
+ };