@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.
- package/LICENSE +21 -21
- package/README.md +84 -20
- package/agents/ez-observer-agent.md +260 -0
- package/agents/ez-release-agent.md +333 -0
- package/agents/ez-requirements-agent.md +377 -0
- package/agents/ez-scrum-master-agent.md +242 -0
- package/agents/ez-tech-lead-agent.md +267 -0
- package/bin/install.js +3221 -3230
- package/commands/ez/arch-review.md +102 -0
- package/commands/ez/execute-phase.md +11 -0
- package/commands/ez/export-session.md +79 -0
- package/commands/ez/gather-requirements.md +117 -0
- package/commands/ez/git-workflow.md +72 -0
- package/commands/ez/hotfix.md +120 -0
- package/commands/ez/import-session.md +82 -0
- package/commands/ez/join-discord.md +18 -18
- package/commands/ez/list-sessions.md +96 -0
- package/commands/ez/package-manager.md +316 -0
- package/commands/ez/plan-phase.md +9 -1
- package/commands/ez/preflight.md +79 -0
- package/commands/ez/progress.md +13 -1
- package/commands/ez/release.md +153 -0
- package/commands/ez/resume.md +107 -0
- package/commands/ez/standup.md +85 -0
- package/ez-agents/bin/ez-tools.cjs +1095 -716
- package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
- package/ez-agents/bin/lib/audit-exec.cjs +7 -2
- package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
- package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
- package/ez-agents/bin/lib/config.cjs +190 -190
- package/ez-agents/bin/lib/content-scanner.cjs +238 -0
- package/ez-agents/bin/lib/context-cache.cjs +154 -0
- package/ez-agents/bin/lib/context-errors.cjs +71 -0
- package/ez-agents/bin/lib/context-manager.cjs +220 -0
- package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
- package/ez-agents/bin/lib/file-access.cjs +207 -0
- package/ez-agents/bin/lib/file-lock.cjs +236 -236
- package/ez-agents/bin/lib/frontmatter.cjs +299 -299
- package/ez-agents/bin/lib/fs-utils.cjs +153 -153
- package/ez-agents/bin/lib/git-errors.cjs +83 -0
- package/ez-agents/bin/lib/git-utils.cjs +118 -0
- package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
- package/ez-agents/bin/lib/index.cjs +157 -113
- package/ez-agents/bin/lib/init.cjs +757 -757
- package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
- package/ez-agents/bin/lib/logger.cjs +124 -124
- package/ez-agents/bin/lib/memory-compression.cjs +256 -0
- package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
- package/ez-agents/bin/lib/milestone.cjs +241 -241
- package/ez-agents/bin/lib/model-provider.cjs +241 -241
- package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
- package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
- package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
- package/ez-agents/bin/lib/phase.cjs +925 -925
- package/ez-agents/bin/lib/planning-write.cjs +107 -107
- package/ez-agents/bin/lib/release-validator.cjs +614 -0
- package/ez-agents/bin/lib/retry.cjs +119 -119
- package/ez-agents/bin/lib/roadmap.cjs +306 -306
- package/ez-agents/bin/lib/safe-exec.cjs +128 -128
- package/ez-agents/bin/lib/safe-path.cjs +130 -130
- package/ez-agents/bin/lib/session-chain.cjs +304 -0
- package/ez-agents/bin/lib/session-errors.cjs +81 -0
- package/ez-agents/bin/lib/session-export.cjs +251 -0
- package/ez-agents/bin/lib/session-import.cjs +262 -0
- package/ez-agents/bin/lib/session-manager.cjs +280 -0
- package/ez-agents/bin/lib/state.cjs +736 -736
- package/ez-agents/bin/lib/temp-file.cjs +239 -239
- package/ez-agents/bin/lib/template.cjs +223 -223
- package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
- package/ez-agents/bin/lib/test-graceful.cjs +93 -93
- package/ez-agents/bin/lib/test-logger.cjs +60 -60
- package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
- package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
- package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
- package/ez-agents/bin/lib/tier-manager.cjs +428 -0
- package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
- package/ez-agents/bin/lib/url-fetch.cjs +170 -0
- package/ez-agents/bin/lib/verify.cjs +15 -1
- package/ez-agents/references/checkpoints.md +776 -776
- package/ez-agents/references/continuation-format.md +249 -249
- package/ez-agents/references/metrics-schema.md +118 -0
- package/ez-agents/references/planning-config.md +140 -0
- package/ez-agents/references/questioning.md +162 -162
- package/ez-agents/references/tdd.md +263 -263
- package/ez-agents/references/tier-strategy.md +103 -0
- package/ez-agents/templates/bdd-feature.md +173 -0
- package/ez-agents/templates/codebase/concerns.md +310 -310
- package/ez-agents/templates/codebase/conventions.md +307 -307
- package/ez-agents/templates/codebase/integrations.md +280 -280
- package/ez-agents/templates/codebase/stack.md +186 -186
- package/ez-agents/templates/codebase/testing.md +480 -480
- package/ez-agents/templates/config.json +37 -37
- package/ez-agents/templates/continue-here.md +78 -78
- package/ez-agents/templates/discussion.md +68 -0
- package/ez-agents/templates/incident-runbook.md +205 -0
- package/ez-agents/templates/milestone-archive.md +123 -123
- package/ez-agents/templates/milestone.md +115 -115
- package/ez-agents/templates/release-checklist.md +133 -0
- package/ez-agents/templates/requirements.md +231 -231
- package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
- package/ez-agents/templates/research-project/FEATURES.md +147 -147
- package/ez-agents/templates/research-project/PITFALLS.md +200 -200
- package/ez-agents/templates/research-project/STACK.md +120 -120
- package/ez-agents/templates/research-project/SUMMARY.md +170 -170
- package/ez-agents/templates/retrospective.md +54 -54
- package/ez-agents/templates/roadmap.md +202 -202
- package/ez-agents/templates/rollback-plan.md +201 -0
- package/ez-agents/templates/summary-minimal.md +41 -41
- package/ez-agents/templates/summary-standard.md +48 -48
- package/ez-agents/templates/summary.md +248 -248
- package/ez-agents/templates/user-setup.md +311 -311
- package/ez-agents/templates/verification-report.md +322 -322
- package/ez-agents/workflows/add-phase.md +112 -112
- package/ez-agents/workflows/add-tests.md +351 -351
- package/ez-agents/workflows/add-todo.md +158 -158
- package/ez-agents/workflows/arch-review.md +54 -0
- package/ez-agents/workflows/audit-milestone.md +332 -332
- package/ez-agents/workflows/autonomous.md +131 -30
- package/ez-agents/workflows/check-todos.md +177 -177
- package/ez-agents/workflows/cleanup.md +152 -152
- package/ez-agents/workflows/complete-milestone.md +766 -766
- package/ez-agents/workflows/diagnose-issues.md +219 -219
- package/ez-agents/workflows/discovery-phase.md +289 -289
- package/ez-agents/workflows/discuss-phase.md +762 -762
- package/ez-agents/workflows/execute-phase.md +513 -468
- package/ez-agents/workflows/execute-plan.md +483 -483
- package/ez-agents/workflows/export-session.md +255 -0
- package/ez-agents/workflows/gather-requirements.md +206 -0
- package/ez-agents/workflows/health.md +159 -159
- package/ez-agents/workflows/help.md +584 -492
- package/ez-agents/workflows/hotfix.md +291 -0
- package/ez-agents/workflows/import-session.md +303 -0
- package/ez-agents/workflows/insert-phase.md +130 -130
- package/ez-agents/workflows/list-phase-assumptions.md +178 -178
- package/ez-agents/workflows/map-codebase.md +316 -316
- package/ez-agents/workflows/new-milestone.md +339 -10
- package/ez-agents/workflows/new-project.md +293 -299
- package/ez-agents/workflows/node-repair.md +92 -92
- package/ez-agents/workflows/pause-work.md +122 -122
- package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
- package/ez-agents/workflows/plan-phase.md +673 -651
- package/ez-agents/workflows/progress.md +372 -382
- package/ez-agents/workflows/quick.md +610 -610
- package/ez-agents/workflows/release.md +253 -0
- package/ez-agents/workflows/remove-phase.md +155 -155
- package/ez-agents/workflows/research-phase.md +74 -74
- package/ez-agents/workflows/resume-project.md +307 -307
- package/ez-agents/workflows/resume-session.md +215 -0
- package/ez-agents/workflows/set-profile.md +81 -81
- package/ez-agents/workflows/settings.md +242 -242
- package/ez-agents/workflows/standup.md +64 -0
- package/ez-agents/workflows/stats.md +57 -57
- package/ez-agents/workflows/transition.md +544 -544
- package/ez-agents/workflows/ui-phase.md +290 -290
- package/ez-agents/workflows/ui-review.md +157 -157
- package/ez-agents/workflows/update.md +320 -320
- package/ez-agents/workflows/validate-phase.md +167 -167
- package/ez-agents/workflows/verify-phase.md +243 -243
- package/ez-agents/workflows/verify-work.md +584 -584
- package/package.json +10 -4
- package/scripts/build-hooks.js +43 -43
- 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;
|