@howlil/ez-agents 3.4.1 → 3.5.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 (162) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +84 -20
  3. package/agents/ez-observer-agent.md +260 -0
  4. package/agents/ez-release-agent.md +333 -0
  5. package/agents/ez-requirements-agent.md +377 -0
  6. package/agents/ez-scrum-master-agent.md +242 -0
  7. package/agents/ez-tech-lead-agent.md +267 -0
  8. package/bin/install.js +3221 -3230
  9. package/commands/ez/arch-review.md +102 -0
  10. package/commands/ez/execute-phase.md +11 -0
  11. package/commands/ez/export-session.md +79 -0
  12. package/commands/ez/gather-requirements.md +117 -0
  13. package/commands/ez/git-workflow.md +72 -0
  14. package/commands/ez/hotfix.md +120 -0
  15. package/commands/ez/import-session.md +82 -0
  16. package/commands/ez/join-discord.md +18 -18
  17. package/commands/ez/list-sessions.md +96 -0
  18. package/commands/ez/package-manager.md +316 -0
  19. package/commands/ez/plan-phase.md +9 -1
  20. package/commands/ez/preflight.md +79 -0
  21. package/commands/ez/progress.md +13 -1
  22. package/commands/ez/release.md +153 -0
  23. package/commands/ez/resume.md +107 -0
  24. package/commands/ez/standup.md +85 -0
  25. package/ez-agents/bin/ez-tools.cjs +1095 -716
  26. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  27. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  28. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  29. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  30. package/ez-agents/bin/lib/config.cjs +190 -190
  31. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  32. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  33. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  34. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  35. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  36. package/ez-agents/bin/lib/file-access.cjs +207 -0
  37. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  38. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  39. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  40. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  41. package/ez-agents/bin/lib/git-utils.cjs +118 -0
  42. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  43. package/ez-agents/bin/lib/index.cjs +157 -113
  44. package/ez-agents/bin/lib/init.cjs +757 -757
  45. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  46. package/ez-agents/bin/lib/logger.cjs +124 -124
  47. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  48. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  49. package/ez-agents/bin/lib/milestone.cjs +241 -241
  50. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  51. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  52. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  53. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  54. package/ez-agents/bin/lib/phase.cjs +925 -925
  55. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  56. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  57. package/ez-agents/bin/lib/retry.cjs +119 -119
  58. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  59. package/ez-agents/bin/lib/safe-exec.cjs +128 -128
  60. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  61. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  62. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  63. package/ez-agents/bin/lib/session-export.cjs +251 -0
  64. package/ez-agents/bin/lib/session-import.cjs +262 -0
  65. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  66. package/ez-agents/bin/lib/state.cjs +736 -736
  67. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  68. package/ez-agents/bin/lib/template.cjs +223 -223
  69. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  70. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  71. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  72. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  73. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  74. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  75. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  76. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  77. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  78. package/ez-agents/bin/lib/verify.cjs +15 -1
  79. package/ez-agents/references/checkpoints.md +776 -776
  80. package/ez-agents/references/continuation-format.md +249 -249
  81. package/ez-agents/references/metrics-schema.md +118 -0
  82. package/ez-agents/references/planning-config.md +140 -0
  83. package/ez-agents/references/questioning.md +162 -162
  84. package/ez-agents/references/tdd.md +263 -263
  85. package/ez-agents/references/tier-strategy.md +103 -0
  86. package/ez-agents/templates/bdd-feature.md +173 -0
  87. package/ez-agents/templates/codebase/concerns.md +310 -310
  88. package/ez-agents/templates/codebase/conventions.md +307 -307
  89. package/ez-agents/templates/codebase/integrations.md +280 -280
  90. package/ez-agents/templates/codebase/stack.md +186 -186
  91. package/ez-agents/templates/codebase/testing.md +480 -480
  92. package/ez-agents/templates/config.json +37 -37
  93. package/ez-agents/templates/continue-here.md +78 -78
  94. package/ez-agents/templates/discussion.md +68 -0
  95. package/ez-agents/templates/incident-runbook.md +205 -0
  96. package/ez-agents/templates/milestone-archive.md +123 -123
  97. package/ez-agents/templates/milestone.md +115 -115
  98. package/ez-agents/templates/release-checklist.md +133 -0
  99. package/ez-agents/templates/requirements.md +231 -231
  100. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  101. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  102. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  103. package/ez-agents/templates/research-project/STACK.md +120 -120
  104. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  105. package/ez-agents/templates/retrospective.md +54 -54
  106. package/ez-agents/templates/roadmap.md +202 -202
  107. package/ez-agents/templates/rollback-plan.md +201 -0
  108. package/ez-agents/templates/summary-minimal.md +41 -41
  109. package/ez-agents/templates/summary-standard.md +48 -48
  110. package/ez-agents/templates/summary.md +248 -248
  111. package/ez-agents/templates/user-setup.md +311 -311
  112. package/ez-agents/templates/verification-report.md +322 -322
  113. package/ez-agents/workflows/add-phase.md +112 -112
  114. package/ez-agents/workflows/add-tests.md +351 -351
  115. package/ez-agents/workflows/add-todo.md +158 -158
  116. package/ez-agents/workflows/arch-review.md +54 -0
  117. package/ez-agents/workflows/audit-milestone.md +332 -332
  118. package/ez-agents/workflows/autonomous.md +131 -30
  119. package/ez-agents/workflows/check-todos.md +177 -177
  120. package/ez-agents/workflows/cleanup.md +152 -152
  121. package/ez-agents/workflows/complete-milestone.md +766 -766
  122. package/ez-agents/workflows/diagnose-issues.md +219 -219
  123. package/ez-agents/workflows/discovery-phase.md +289 -289
  124. package/ez-agents/workflows/discuss-phase.md +762 -762
  125. package/ez-agents/workflows/execute-phase.md +513 -468
  126. package/ez-agents/workflows/execute-plan.md +483 -483
  127. package/ez-agents/workflows/export-session.md +255 -0
  128. package/ez-agents/workflows/gather-requirements.md +206 -0
  129. package/ez-agents/workflows/health.md +159 -159
  130. package/ez-agents/workflows/help.md +584 -492
  131. package/ez-agents/workflows/hotfix.md +291 -0
  132. package/ez-agents/workflows/import-session.md +303 -0
  133. package/ez-agents/workflows/insert-phase.md +130 -130
  134. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  135. package/ez-agents/workflows/map-codebase.md +316 -316
  136. package/ez-agents/workflows/new-milestone.md +339 -10
  137. package/ez-agents/workflows/new-project.md +293 -299
  138. package/ez-agents/workflows/node-repair.md +92 -92
  139. package/ez-agents/workflows/pause-work.md +122 -122
  140. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  141. package/ez-agents/workflows/plan-phase.md +673 -651
  142. package/ez-agents/workflows/progress.md +372 -382
  143. package/ez-agents/workflows/quick.md +610 -610
  144. package/ez-agents/workflows/release.md +253 -0
  145. package/ez-agents/workflows/remove-phase.md +155 -155
  146. package/ez-agents/workflows/research-phase.md +74 -74
  147. package/ez-agents/workflows/resume-project.md +307 -307
  148. package/ez-agents/workflows/resume-session.md +215 -0
  149. package/ez-agents/workflows/set-profile.md +81 -81
  150. package/ez-agents/workflows/settings.md +242 -242
  151. package/ez-agents/workflows/standup.md +64 -0
  152. package/ez-agents/workflows/stats.md +57 -57
  153. package/ez-agents/workflows/transition.md +544 -544
  154. package/ez-agents/workflows/ui-phase.md +290 -290
  155. package/ez-agents/workflows/ui-review.md +157 -157
  156. package/ez-agents/workflows/update.md +320 -320
  157. package/ez-agents/workflows/validate-phase.md +167 -167
  158. package/ez-agents/workflows/verify-phase.md +243 -243
  159. package/ez-agents/workflows/verify-work.md +584 -584
  160. package/package.json +10 -4
  161. package/scripts/build-hooks.js +43 -43
  162. package/scripts/run-tests.cjs +29 -29
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Session Import — Import session data from exported files
5
+ *
6
+ * Validates session structure and chain integrity
7
+ * Supports model-specific adapters
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const { SessionImportError, SessionNotFoundError } = require('./session-errors.cjs');
12
+ const { defaultLogger: logger } = require('./logger.cjs');
13
+
14
+ class SessionImport {
15
+ /**
16
+ * Create a SessionImport instance
17
+ * @param {Object} sessionManager - SessionManager instance
18
+ */
19
+ constructor(sessionManager) {
20
+ this.sessionManager = sessionManager;
21
+ }
22
+
23
+ /**
24
+ * Import a session from file
25
+ * @param {string} sessionFile - Path to session file
26
+ * @param {Object} options - Import options
27
+ * @param {string} [options.sourceModel] - Source model for adapter
28
+ * @returns {Object} Import result with sessionId and warnings
29
+ */
30
+ import(sessionFile, options = {}) {
31
+ const { sourceModel } = options;
32
+ const warnings = [];
33
+
34
+ // Read and parse file
35
+ let importedData;
36
+ try {
37
+ const content = fs.readFileSync(sessionFile, 'utf-8');
38
+ importedData = JSON.parse(content);
39
+ } catch (err) {
40
+ throw new SessionImportError(`Failed to parse JSON: ${err.message}`, [err.message]);
41
+ }
42
+
43
+ // Handle model-specific format conversion
44
+ if (sourceModel) {
45
+ importedData = this.importFromModelSpecificFormat(importedData, sourceModel);
46
+ }
47
+
48
+ // Validate structure
49
+ const structureValidation = this.validateStructure(importedData);
50
+ if (!structureValidation.valid) {
51
+ throw new SessionImportError('Invalid session structure', structureValidation.errors);
52
+ }
53
+
54
+ // Validate session chain
55
+ const chainValidation = this.validateSessionChain(importedData);
56
+ if (!chainValidation.valid) {
57
+ throw new SessionImportError('Invalid session chain', chainValidation.errors);
58
+ }
59
+
60
+ // Validate chain links exist
61
+ const linksValidation = this.validateChainLinksExist(importedData);
62
+ if (linksValidation.warnings && linksValidation.warnings.length > 0) {
63
+ warnings.push(...linksValidation.warnings);
64
+ }
65
+
66
+ // Create new session
67
+ const newSessionId = this.sessionManager.createSession({
68
+ model: importedData.metadata?.model,
69
+ phase: importedData.metadata?.phase,
70
+ plan: importedData.metadata?.plan
71
+ });
72
+
73
+ // Merge imported context and state
74
+ const updates = {
75
+ context: importedData.context || {},
76
+ state: importedData.state || {}
77
+ };
78
+
79
+ // Add previous session to chain
80
+ const previousSessionId = importedData.metadata?.session_id;
81
+ if (previousSessionId) {
82
+ updates.metadata = {
83
+ session_chain: [previousSessionId]
84
+ };
85
+ }
86
+
87
+ this.sessionManager.updateSession(newSessionId, updates);
88
+
89
+ logger.info('Session imported', { sessionId: newSessionId, sourceFile: sessionFile });
90
+
91
+ return {
92
+ success: true,
93
+ sessionId: newSessionId,
94
+ warnings
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Validate session structure
100
+ * @param {Object} session - Session object
101
+ * @returns {Object} Validation result
102
+ */
103
+ validateStructure(session) {
104
+ const errors = [];
105
+
106
+ // Check for export wrapper or direct session
107
+ const sessionData = session.session || session;
108
+
109
+ if (!sessionData.metadata) {
110
+ errors.push('Missing metadata section');
111
+ } else {
112
+ if (!sessionData.metadata.session_id) {
113
+ errors.push('Missing metadata.session_id');
114
+ }
115
+ if (!sessionData.metadata.started_at) {
116
+ errors.push('Missing metadata.started_at');
117
+ }
118
+ if (!sessionData.metadata.session_version) {
119
+ errors.push('Missing metadata.session_version');
120
+ }
121
+ }
122
+
123
+ if (!sessionData.context) {
124
+ errors.push('Missing context section');
125
+ } else {
126
+ if (!Array.isArray(sessionData.context.tasks)) {
127
+ errors.push('context.tasks must be an array');
128
+ }
129
+ if (!Array.isArray(sessionData.context.decisions)) {
130
+ errors.push('context.decisions must be an array');
131
+ }
132
+ }
133
+
134
+ return {
135
+ valid: errors.length === 0,
136
+ errors
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Validate session chain integrity
142
+ * @param {Object} session - Session object
143
+ * @returns {Object} Validation result
144
+ */
145
+ validateSessionChain(session) {
146
+ const errors = [];
147
+ const sessionData = session.session || session;
148
+
149
+ if (sessionData.metadata?.session_chain) {
150
+ const chain = sessionData.metadata.session_chain;
151
+ const sessionId = sessionData.metadata.session_id;
152
+
153
+ if (!Array.isArray(chain)) {
154
+ errors.push('session_chain must be an array');
155
+ } else if (chain.includes(sessionId)) {
156
+ errors.push('Circular reference detected: session_chain includes current session_id');
157
+ }
158
+ }
159
+
160
+ return {
161
+ valid: errors.length === 0,
162
+ errors
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Validate chain links exist in session manager
168
+ * @param {Object} session - Session object
169
+ * @returns {Object} Validation result with warnings
170
+ */
171
+ validateChainLinksExist(session) {
172
+ const warnings = [];
173
+ const sessionData = session.session || session;
174
+
175
+ if (sessionData.metadata?.session_chain) {
176
+ const chain = sessionData.metadata.session_chain;
177
+ const missingLinks = [];
178
+
179
+ for (const id of chain) {
180
+ const linkedSession = this.sessionManager.loadSession(id);
181
+ if (!linkedSession) {
182
+ missingLinks.push(id);
183
+ }
184
+ }
185
+
186
+ if (missingLinks.length > 0) {
187
+ warnings.push(`Missing chain links: ${missingLinks.join(', ')}`);
188
+ }
189
+ }
190
+
191
+ return {
192
+ valid: true,
193
+ warnings
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Import from model-specific format
199
+ * @param {Object} data - Data to convert
200
+ * @param {string} sourceModel - Source model name
201
+ * @returns {Object} Converted session object
202
+ */
203
+ importFromModelSpecificFormat(data, sourceModel) {
204
+ // If data has session wrapper, use it
205
+ if (data.session) {
206
+ return data.session;
207
+ }
208
+
209
+ // Model-specific adapters (can be extended)
210
+ switch (sourceModel.toLowerCase()) {
211
+ case 'claude':
212
+ return this._adaptClaudeFormat(data);
213
+ case 'qwen':
214
+ return this._adaptQwenFormat(data);
215
+ case 'openai':
216
+ return this._adaptOpenAIFormat(data);
217
+ case 'kimi':
218
+ return this._adaptKimiFormat(data);
219
+ default:
220
+ // Assume standard format
221
+ return data;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Adapt Claude-specific format
227
+ * @private
228
+ */
229
+ _adaptClaudeFormat(data) {
230
+ // Claude format adapter (placeholder)
231
+ return data;
232
+ }
233
+
234
+ /**
235
+ * Adapt Qwen-specific format
236
+ * @private
237
+ */
238
+ _adaptQwenFormat(data) {
239
+ // Qwen format adapter (placeholder)
240
+ return data;
241
+ }
242
+
243
+ /**
244
+ * Adapt OpenAI-specific format
245
+ * @private
246
+ */
247
+ _adaptOpenAIFormat(data) {
248
+ // OpenAI format adapter (placeholder)
249
+ return data;
250
+ }
251
+
252
+ /**
253
+ * Adapt Kimi-specific format
254
+ * @private
255
+ */
256
+ _adaptKimiFormat(data) {
257
+ // Kimi format adapter (placeholder)
258
+ return data;
259
+ }
260
+ }
261
+
262
+ module.exports = SessionImport;
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Session Manager — Core session state persistence module
5
+ *
6
+ * Manages session lifecycle: create, load, update, end, list
7
+ * Sessions stored in .planning/sessions/session-{timestamp}.json
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { safePlanningWriteSync } = require('./planning-write.cjs');
13
+ const { defaultLogger: logger } = require('./logger.cjs');
14
+
15
+ class SessionManager {
16
+ /**
17
+ * Create a SessionManager instance
18
+ * @param {string} sessionsDir - Directory for session files (default: .planning/sessions)
19
+ */
20
+ constructor(sessionsDir = '.planning/sessions') {
21
+ this.sessionsDir = sessionsDir;
22
+ this._ensureDir();
23
+ }
24
+
25
+ /**
26
+ * Ensure sessions directory exists
27
+ * @private
28
+ */
29
+ _ensureDir() {
30
+ if (!fs.existsSync(this.sessionsDir)) {
31
+ fs.mkdirSync(this.sessionsDir, { recursive: true });
32
+ logger.info('Sessions directory created', { dir: this.sessionsDir });
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Generate session ID from timestamp
38
+ * Format: session-YYYYMMDD-HHMMSS
39
+ * @private
40
+ * @returns {string} Session ID
41
+ */
42
+ _generateSessionId() {
43
+ const now = new Date();
44
+ const timestamp = now.toISOString()
45
+ .replace(/[:.]/g, '-')
46
+ .replace('T', '-')
47
+ .slice(0, -5); // Remove milliseconds and Z
48
+ return `session-${timestamp}`;
49
+ }
50
+
51
+ /**
52
+ * Deep merge source into target
53
+ * @private
54
+ * @param {Object} target - Target object
55
+ * @param {Object} source - Source object
56
+ * @returns {Object} Merged object
57
+ */
58
+ _deepMerge(target, source) {
59
+ const result = { ...target };
60
+ for (const key in source) {
61
+ if (source.hasOwnProperty(key)) {
62
+ if (source[key] instanceof Object && key !== null) {
63
+ result[key] = this._deepMerge(result[key] || {}, source[key]);
64
+ } else {
65
+ result[key] = source[key];
66
+ }
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ /**
73
+ * Create a new session
74
+ * @param {Object} options - Session options
75
+ * @param {string} [options.model] - Model identifier
76
+ * @param {number} [options.phase] - Phase number
77
+ * @param {number} [options.plan] - Plan number
78
+ * @param {string} [options.objective] - Session objective
79
+ * @returns {string} Session ID
80
+ */
81
+ createSession(options = {}) {
82
+ const sessionId = this._generateSessionId();
83
+ const sessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
84
+
85
+ const session = {
86
+ metadata: {
87
+ session_id: sessionId,
88
+ session_version: '1.0',
89
+ started_at: new Date().toISOString(),
90
+ ended_at: null,
91
+ model: options.model || null,
92
+ phase: options.phase || null,
93
+ plan: options.plan || null,
94
+ status: 'active',
95
+ session_chain: [],
96
+ token_usage: {
97
+ input: 0,
98
+ output: 0,
99
+ total: 0
100
+ }
101
+ },
102
+ context: {
103
+ transcript: '',
104
+ tasks: [],
105
+ decisions: [],
106
+ file_changes: [],
107
+ open_questions: [],
108
+ blockers: []
109
+ },
110
+ state: {
111
+ current_phase: options.phase || null,
112
+ current_plan: options.plan || null,
113
+ incomplete_tasks: [],
114
+ last_action: null,
115
+ next_recommended_action: options.objective || null
116
+ }
117
+ };
118
+
119
+ try {
120
+ safePlanningWriteSync(sessionPath, JSON.stringify(session, null, 2));
121
+ logger.info('Session created', { sessionId, sessionPath });
122
+ return sessionId;
123
+ } catch (err) {
124
+ logger.error('Failed to create session', { sessionId, error: err.message });
125
+ throw err;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Load a session by ID
131
+ * @param {string} sessionId - Session ID
132
+ * @returns {Object|null} Session object or null if not found
133
+ */
134
+ loadSession(sessionId) {
135
+ const sessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
136
+
137
+ if (!fs.existsSync(sessionPath)) {
138
+ logger.error('Session not found', { sessionId, sessionPath });
139
+ return null;
140
+ }
141
+
142
+ try {
143
+ const content = fs.readFileSync(sessionPath, 'utf-8');
144
+ const session = JSON.parse(content);
145
+ logger.info('Session loaded', { sessionId });
146
+ return session;
147
+ } catch (err) {
148
+ logger.error('Failed to load session', { sessionId, error: err.message });
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get the most recent session
155
+ * @returns {Object|null} Last session or null if none exist
156
+ */
157
+ getLastSession() {
158
+ const sessionFiles = this._getSessionFiles();
159
+
160
+ if (sessionFiles.length === 0) {
161
+ logger.info('No sessions found');
162
+ return null;
163
+ }
164
+
165
+ // Files are sorted, newest first
166
+ const lastFile = sessionFiles[0];
167
+ const sessionId = lastFile.replace('.json', '');
168
+ return this.loadSession(sessionId);
169
+ }
170
+
171
+ /**
172
+ * Update a session with new data
173
+ * @param {string} sessionId - Session ID
174
+ * @param {Object} updates - Updates to merge into session
175
+ * @returns {boolean} True on success, false if session not found
176
+ */
177
+ updateSession(sessionId, updates) {
178
+ const session = this.loadSession(sessionId);
179
+ if (!session) {
180
+ return false;
181
+ }
182
+
183
+ const updatedSession = this._deepMerge(session, updates);
184
+ const sessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
185
+
186
+ try {
187
+ safePlanningWriteSync(sessionPath, JSON.stringify(updatedSession, null, 2));
188
+ logger.info('Session updated', { sessionId });
189
+ return true;
190
+ } catch (err) {
191
+ logger.error('Failed to update session', { sessionId, error: err.message });
192
+ return false;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * End a session
198
+ * @param {string} sessionId - Session ID
199
+ * @param {Object} finalState - Final state information
200
+ * @param {string} [finalState.status] - Final status (default: 'completed')
201
+ * @param {Array} [finalState.incomplete_tasks] - Incomplete tasks
202
+ * @param {string} [finalState.next_recommended_action] - Next recommended action
203
+ * @returns {boolean} True on success, false if session not found
204
+ */
205
+ endSession(sessionId, finalState = {}) {
206
+ const session = this.loadSession(sessionId);
207
+ if (!session) {
208
+ return false;
209
+ }
210
+
211
+ const updates = {
212
+ metadata: {
213
+ ended_at: new Date().toISOString(),
214
+ status: finalState.status || 'completed'
215
+ },
216
+ state: {}
217
+ };
218
+
219
+ if (finalState.incomplete_tasks) {
220
+ updates.state.incomplete_tasks = finalState.incomplete_tasks;
221
+ }
222
+
223
+ if (finalState.next_recommended_action) {
224
+ updates.state.next_recommended_action = finalState.next_recommended_action;
225
+ }
226
+
227
+ return this.updateSession(sessionId, updates);
228
+ }
229
+
230
+ /**
231
+ * List all sessions
232
+ * @returns {Array} Array of session metadata sorted by date (newest first)
233
+ */
234
+ listSessions() {
235
+ const sessionFiles = this._getSessionFiles();
236
+ const sessions = [];
237
+
238
+ for (const file of sessionFiles) {
239
+ const sessionId = file.replace('.json', '');
240
+ const session = this.loadSession(sessionId);
241
+ if (session) {
242
+ sessions.push({
243
+ session_id: session.metadata.session_id,
244
+ started_at: session.metadata.started_at,
245
+ ended_at: session.metadata.ended_at,
246
+ model: session.metadata.model,
247
+ phase: session.metadata.phase,
248
+ plan: session.metadata.plan,
249
+ status: session.metadata.status
250
+ });
251
+ }
252
+ }
253
+
254
+ return sessions;
255
+ }
256
+
257
+ /**
258
+ * Get session files sorted by name (newest first)
259
+ * @private
260
+ * @returns {Array} Array of filenames
261
+ */
262
+ _getSessionFiles() {
263
+ if (!fs.existsSync(this.sessionsDir)) {
264
+ return [];
265
+ }
266
+
267
+ try {
268
+ const files = fs.readdirSync(this.sessionsDir)
269
+ .filter(file => /^session-.*\.json$/.test(file))
270
+ .sort()
271
+ .reverse();
272
+ return files;
273
+ } catch (err) {
274
+ logger.error('Failed to read sessions directory', { error: err.message });
275
+ return [];
276
+ }
277
+ }
278
+ }
279
+
280
+ module.exports = SessionManager;