agileflow 2.89.3 → 2.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/placeholder-registry.js +617 -0
  3. package/lib/smart-json-file.js +228 -1
  4. package/lib/table-formatter.js +519 -0
  5. package/lib/transient-status.js +374 -0
  6. package/lib/ui-manager.js +612 -0
  7. package/lib/validate-args.js +213 -0
  8. package/lib/validate-names.js +143 -0
  9. package/lib/validate-paths.js +434 -0
  10. package/lib/validate.js +37 -737
  11. package/package.json +3 -1
  12. package/scripts/check-update.js +17 -3
  13. package/scripts/lib/sessionRegistry.js +678 -0
  14. package/scripts/session-manager.js +77 -10
  15. package/scripts/tui/App.js +151 -0
  16. package/scripts/tui/index.js +31 -0
  17. package/scripts/tui/lib/crashRecovery.js +304 -0
  18. package/scripts/tui/lib/eventStream.js +309 -0
  19. package/scripts/tui/lib/keyboard.js +261 -0
  20. package/scripts/tui/lib/loopControl.js +371 -0
  21. package/scripts/tui/panels/OutputPanel.js +242 -0
  22. package/scripts/tui/panels/SessionPanel.js +170 -0
  23. package/scripts/tui/panels/TracePanel.js +298 -0
  24. package/scripts/tui/simple-tui.js +390 -0
  25. package/tools/cli/commands/config.js +7 -31
  26. package/tools/cli/commands/doctor.js +28 -39
  27. package/tools/cli/commands/list.js +47 -35
  28. package/tools/cli/commands/status.js +20 -38
  29. package/tools/cli/commands/tui.js +59 -0
  30. package/tools/cli/commands/uninstall.js +12 -39
  31. package/tools/cli/installers/core/installer.js +13 -0
  32. package/tools/cli/lib/command-context.js +382 -0
  33. package/tools/cli/lib/config-manager.js +394 -0
  34. package/tools/cli/lib/ide-registry.js +186 -0
  35. package/tools/cli/lib/npm-utils.js +17 -3
  36. package/tools/cli/lib/self-update.js +148 -0
  37. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -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,151 @@
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(Text, { bold: true, color: 'cyan' }, title),
75
+ lastAction && React.createElement(Text, { dimColor: true }, ` [${lastAction.action}]`)
76
+ ),
77
+ // Main content area
78
+ React.createElement(
79
+ Box,
80
+ {
81
+ flexDirection: 'column',
82
+ flexGrow: 1,
83
+ paddingX: 1,
84
+ paddingY: 1,
85
+ },
86
+ showHelp ? React.createElement(HelpPanel, { bindings }) : children
87
+ ),
88
+ // Footer with key bindings
89
+ showFooter &&
90
+ React.createElement(
91
+ Box,
92
+ {
93
+ borderStyle: 'single',
94
+ borderColor: 'gray',
95
+ paddingX: 1,
96
+ justifyContent: 'space-between',
97
+ },
98
+ React.createElement(
99
+ Box,
100
+ { flexDirection: 'row' },
101
+ footerBindings.map((binding, i) =>
102
+ React.createElement(
103
+ Text,
104
+ { key: `binding-${i}`, dimColor: true },
105
+ i > 0 ? ' | ' : '',
106
+ binding
107
+ )
108
+ )
109
+ ),
110
+ React.createElement(Text, { dimColor: true }, '1-9:Sessions | AgileFlow v2.89.3')
111
+ )
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Help Panel component
117
+ */
118
+ function HelpPanel({ bindings = DEFAULT_BINDINGS }) {
119
+ const groups = {
120
+ 'Loop Control': ['start', 'pause', 'resume'],
121
+ View: ['trace', 'help'],
122
+ Navigation: ['quit'],
123
+ Sessions: ['session1', 'session2', 'session3'],
124
+ };
125
+
126
+ return React.createElement(
127
+ Box,
128
+ { flexDirection: 'column', padding: 1 },
129
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Key Bindings'),
130
+ React.createElement(Box, { marginTop: 1 }),
131
+ Object.entries(groups).map(([groupName, actions]) =>
132
+ React.createElement(
133
+ Box,
134
+ { key: groupName, flexDirection: 'column', marginBottom: 1 },
135
+ React.createElement(Text, { bold: true }, groupName + ':'),
136
+ actions.map(action => {
137
+ const binding = bindings[action];
138
+ if (!binding) return null;
139
+ return React.createElement(
140
+ Text,
141
+ { key: action, dimColor: true },
142
+ ` ${binding.key.toUpperCase()} - ${binding.description}`
143
+ );
144
+ })
145
+ )
146
+ ),
147
+ React.createElement(Text, { dimColor: true, marginTop: 1 }, 'Press ? to close help')
148
+ );
149
+ }
150
+
151
+ module.exports = { App, HelpPanel };
@@ -0,0 +1,31 @@
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
+ // Use the simple TUI implementation (pure Node.js, no React dependencies)
24
+ const { main } = require('./simple-tui');
25
+
26
+ // Run if executed directly
27
+ if (require.main === module) {
28
+ main();
29
+ }
30
+
31
+ module.exports = { main };
@@ -0,0 +1,304 @@
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(
155
+ checkpoint.recovery_info?.last_checkpoint || checkpoint.created_at
156
+ );
157
+ const now = new Date();
158
+ const hoursSinceCheckpoint = (now - lastCheckpoint) / (1000 * 60 * 60);
159
+
160
+ if (hoursSinceCheckpoint > 24) {
161
+ return { needed: false, reason: 'checkpoint_expired', checkpoint };
162
+ }
163
+
164
+ // Check if loop was in progress
165
+ const loopState = checkpoint.loop_state;
166
+ if (!loopState || loopState.iteration === 0) {
167
+ return { needed: false, reason: 'loop_not_started', checkpoint };
168
+ }
169
+
170
+ // Check current session state
171
+ const statePath = getSessionStatePath();
172
+ const stateResult = safeReadJSON(statePath, { defaultValue: {} });
173
+ const currentState = stateResult.ok ? stateResult.data : {};
174
+
175
+ // If loop is still enabled and matches checkpoint, no recovery needed
176
+ if (currentState.ralph_loop && currentState.ralph_loop.enabled) {
177
+ if (currentState.ralph_loop.iteration === loopState.iteration) {
178
+ return { needed: false, reason: 'loop_still_active', checkpoint };
179
+ }
180
+ }
181
+
182
+ // Loop was in progress but not active now - recovery needed
183
+ return {
184
+ needed: true,
185
+ reason: 'incomplete_loop',
186
+ checkpoint,
187
+ recovery_options: {
188
+ resume: {
189
+ iteration: loopState.iteration,
190
+ story: loopState.current_story,
191
+ epic: loopState.epic,
192
+ },
193
+ fresh: {
194
+ message: 'Start fresh from beginning of epic',
195
+ },
196
+ },
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Resume from checkpoint
202
+ */
203
+ function resumeFromCheckpoint(sessionId = 'default') {
204
+ const checkpointResult = loadCheckpoint(sessionId);
205
+
206
+ if (!checkpointResult.ok || !checkpointResult.checkpoint) {
207
+ return { ok: false, error: 'No valid checkpoint to resume from' };
208
+ }
209
+
210
+ const checkpoint = checkpointResult.checkpoint;
211
+ const loopState = checkpoint.loop_state;
212
+
213
+ // Restore loop state to session state
214
+ const statePath = getSessionStatePath();
215
+ const stateResult = safeReadJSON(statePath, { defaultValue: {} });
216
+ const state = stateResult.ok ? stateResult.data : {};
217
+
218
+ state.ralph_loop = {
219
+ enabled: true,
220
+ epic: loopState.epic,
221
+ current_story: loopState.current_story,
222
+ iteration: loopState.iteration,
223
+ max_iterations: loopState.max_iterations,
224
+ visual_mode: loopState.visual_mode,
225
+ coverage_mode: loopState.coverage_mode,
226
+ coverage_threshold: loopState.coverage_threshold,
227
+ coverage_current: loopState.coverage_current,
228
+ conditions: loopState.conditions,
229
+ started_at: loopState.started_at,
230
+ resumed_at: new Date().toISOString(),
231
+ resumed_from_checkpoint: true,
232
+ };
233
+
234
+ safeWriteJSON(statePath, state);
235
+
236
+ return {
237
+ ok: true,
238
+ resumed: true,
239
+ iteration: loopState.iteration,
240
+ story: loopState.current_story,
241
+ epic: loopState.epic,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Clear checkpoint (on clean completion)
247
+ */
248
+ function clearCheckpoint(sessionId = 'default') {
249
+ const checkpointPath = getCheckpointPath(sessionId);
250
+
251
+ if (fs.existsSync(checkpointPath)) {
252
+ fs.unlinkSync(checkpointPath);
253
+ return { ok: true, cleared: true };
254
+ }
255
+
256
+ return { ok: true, cleared: false };
257
+ }
258
+
259
+ /**
260
+ * Start fresh (clear checkpoint and any existing loop state)
261
+ */
262
+ function startFresh(sessionId = 'default') {
263
+ // Clear checkpoint
264
+ clearCheckpoint(sessionId);
265
+
266
+ // Clear loop state
267
+ const statePath = getSessionStatePath();
268
+ const stateResult = safeReadJSON(statePath, { defaultValue: {} });
269
+ const state = stateResult.ok ? stateResult.data : {};
270
+
271
+ if (state.ralph_loop) {
272
+ delete state.ralph_loop;
273
+ safeWriteJSON(statePath, state);
274
+ }
275
+
276
+ return { ok: true, cleared: true };
277
+ }
278
+
279
+ /**
280
+ * Get recovery status summary
281
+ */
282
+ function getRecoveryStatus(sessionId = 'default') {
283
+ const recovery = checkRecoveryNeeded(sessionId);
284
+ const checkpoint = loadCheckpoint(sessionId);
285
+
286
+ return {
287
+ recoveryNeeded: recovery.needed,
288
+ reason: recovery.reason,
289
+ hasCheckpoint: checkpoint.exists,
290
+ checkpoint: checkpoint.checkpoint,
291
+ options: recovery.recovery_options,
292
+ };
293
+ }
294
+
295
+ module.exports = {
296
+ getCheckpointPath,
297
+ createCheckpoint,
298
+ loadCheckpoint,
299
+ checkRecoveryNeeded,
300
+ resumeFromCheckpoint,
301
+ clearCheckpoint,
302
+ startFresh,
303
+ getRecoveryStatus,
304
+ };