@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,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Chain — Navigate linked sessions
|
|
5
|
+
*
|
|
6
|
+
* Provides chain navigation, visualization, and repair capabilities
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { SessionChainError } = require('./session-errors.cjs');
|
|
12
|
+
const { defaultLogger: logger } = require('./logger.cjs');
|
|
13
|
+
|
|
14
|
+
class SessionChain {
|
|
15
|
+
/**
|
|
16
|
+
* Create a SessionChain instance
|
|
17
|
+
* @param {Object} sessionManager - SessionManager instance
|
|
18
|
+
*/
|
|
19
|
+
constructor(sessionManager) {
|
|
20
|
+
this.sessionManager = sessionManager;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Navigate to adjacent session in chain
|
|
25
|
+
* @param {string} sessionId - Current session ID
|
|
26
|
+
* @param {string} direction - Navigation direction ('previous' or 'next')
|
|
27
|
+
* @returns {Object|null} Adjacent session or null
|
|
28
|
+
*/
|
|
29
|
+
navigate(sessionId, direction) {
|
|
30
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
31
|
+
if (!session) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const chain = session.metadata?.session_chain || [];
|
|
36
|
+
const currentIndex = chain.indexOf(sessionId);
|
|
37
|
+
|
|
38
|
+
if (currentIndex === -1) {
|
|
39
|
+
// Session not in chain, check if it's the last one
|
|
40
|
+
logger.warn('Session not in chain', { sessionId });
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (direction === 'previous') {
|
|
45
|
+
if (currentIndex > 0) {
|
|
46
|
+
const previousId = chain[currentIndex - 1];
|
|
47
|
+
return this.sessionManager.loadSession(previousId);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (direction === 'next') {
|
|
53
|
+
if (currentIndex < chain.length - 1) {
|
|
54
|
+
const nextId = chain[currentIndex + 1];
|
|
55
|
+
return this.sessionManager.loadSession(nextId);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new SessionChainError(`Invalid direction: ${direction}`, chain);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get full chain as array of session objects
|
|
65
|
+
* @param {string} sessionId - Session ID in the chain
|
|
66
|
+
* @returns {Array} Array of session objects
|
|
67
|
+
*/
|
|
68
|
+
getChain(sessionId) {
|
|
69
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
70
|
+
if (!session) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const chain = session.metadata?.session_chain || [];
|
|
75
|
+
const chainSessions = [];
|
|
76
|
+
|
|
77
|
+
for (const id of chain) {
|
|
78
|
+
const chainSession = this.sessionManager.loadSession(id);
|
|
79
|
+
if (chainSession) {
|
|
80
|
+
chainSessions.push(chainSession);
|
|
81
|
+
} else {
|
|
82
|
+
logger.warn('Missing session in chain', { id });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Include current session if not already in chain
|
|
87
|
+
if (!chain.includes(sessionId)) {
|
|
88
|
+
chainSessions.push(session);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return chainSessions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get chain visualization string
|
|
96
|
+
* @param {string} sessionId - Session ID
|
|
97
|
+
* @returns {string} Formatted chain visualization
|
|
98
|
+
*/
|
|
99
|
+
getChainVisualization(sessionId) {
|
|
100
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
101
|
+
if (!session) {
|
|
102
|
+
return `Session not found: ${sessionId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const chain = session.metadata?.session_chain || [];
|
|
106
|
+
const currentIndex = chain.indexOf(sessionId);
|
|
107
|
+
|
|
108
|
+
let viz = `Session Chain for ${sessionId}:\n\n`;
|
|
109
|
+
|
|
110
|
+
chain.forEach((id, index) => {
|
|
111
|
+
const chainSession = this.sessionManager.loadSession(id);
|
|
112
|
+
const startedAt = chainSession?.metadata?.started_at || 'Unknown';
|
|
113
|
+
const status = chainSession?.metadata?.status || 'unknown';
|
|
114
|
+
const marker = index === currentIndex ? ' <-- Current' : '';
|
|
115
|
+
viz += `[${index + 1}] ${id} (${startedAt}) - ${status}${marker}\n`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!chain.includes(sessionId)) {
|
|
119
|
+
const startedAt = session.metadata?.started_at || 'Unknown';
|
|
120
|
+
const status = session.metadata?.status || 'unknown';
|
|
121
|
+
viz += `[${chain.length + 1}] ${sessionId} (${startedAt}) - ${status} <-- Current\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
viz += `\nNavigation:\n`;
|
|
125
|
+
if (currentIndex > 0) {
|
|
126
|
+
viz += `- Previous: ${chain[currentIndex - 1]}\n`;
|
|
127
|
+
} else {
|
|
128
|
+
viz += `- Previous: none\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (currentIndex < chain.length - 1) {
|
|
132
|
+
viz += `- Next: ${chain[currentIndex + 1]}\n`;
|
|
133
|
+
} else {
|
|
134
|
+
viz += `- Next: none\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return viz;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Repair broken chain links
|
|
142
|
+
* @param {string} sessionId - Session ID
|
|
143
|
+
* @returns {Object} Repair result with warnings
|
|
144
|
+
*/
|
|
145
|
+
repairChain(sessionId) {
|
|
146
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
147
|
+
if (!session) {
|
|
148
|
+
throw new SessionChainError(`Session not found: ${sessionId}`, []);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const chain = session.metadata?.session_chain || [];
|
|
152
|
+
const warnings = [];
|
|
153
|
+
const repaired = [];
|
|
154
|
+
|
|
155
|
+
// Get all available sessions
|
|
156
|
+
const allSessions = this.sessionManager.listSessions();
|
|
157
|
+
const availableIds = new Set(allSessions.map(s => s.session_id));
|
|
158
|
+
|
|
159
|
+
// Find missing links
|
|
160
|
+
const missingLinks = [];
|
|
161
|
+
for (const id of chain) {
|
|
162
|
+
if (!availableIds.has(id)) {
|
|
163
|
+
missingLinks.push(id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (missingLinks.length === 0) {
|
|
168
|
+
return { repaired: false, warnings: ['Chain is intact'] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Attempt to repair by finding closest timestamp match
|
|
172
|
+
for (const missingId of missingLinks) {
|
|
173
|
+
const match = this._findClosestSessionMatch(missingId, allSessions);
|
|
174
|
+
if (match) {
|
|
175
|
+
logger.info('Auto-repaired chain link', { missing: missingId, found: match.session_id });
|
|
176
|
+
repaired.push({ missing: missingId, found: match.session_id });
|
|
177
|
+
} else {
|
|
178
|
+
warnings.push(`Unrecoverable link: ${missingId}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Update chain with repaired links
|
|
183
|
+
if (repaired.length > 0) {
|
|
184
|
+
const newChain = chain.map(id => {
|
|
185
|
+
const repair = repaired.find(r => r.missing === id);
|
|
186
|
+
return repair ? repair.found : id;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.sessionManager.updateSession(sessionId, {
|
|
190
|
+
metadata: { session_chain: newChain }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
repaired: repaired.length > 0,
|
|
196
|
+
repairs: repaired,
|
|
197
|
+
warnings
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Find closest session match by timestamp
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
_findClosestSessionMatch(missingId, allSessions) {
|
|
206
|
+
// Extract timestamp from missing ID
|
|
207
|
+
const timestampMatch = missingId.match(/session-(.+)/);
|
|
208
|
+
if (!timestampMatch) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const missingTimestamp = timestampMatch[1];
|
|
213
|
+
|
|
214
|
+
// Find session with closest timestamp
|
|
215
|
+
let closestMatch = null;
|
|
216
|
+
let minDiff = Infinity;
|
|
217
|
+
|
|
218
|
+
for (const session of allSessions) {
|
|
219
|
+
const sessionTimestamp = session.session_id.replace('session-', '');
|
|
220
|
+
const diff = this._compareTimestamps(missingTimestamp, sessionTimestamp);
|
|
221
|
+
|
|
222
|
+
if (diff < minDiff) {
|
|
223
|
+
minDiff = diff;
|
|
224
|
+
closestMatch = session;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Only return match if within reasonable threshold (1 hour)
|
|
229
|
+
if (minDiff < 3600000) {
|
|
230
|
+
return closestMatch;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Compare timestamps and return difference in ms
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
_compareTimestamps(ts1, ts2) {
|
|
241
|
+
try {
|
|
242
|
+
const date1 = new Date(ts1.replace(/-/g, ':').replace('T', ' '));
|
|
243
|
+
const date2 = new Date(ts2.replace(/-/g, ':').replace('T', ' '));
|
|
244
|
+
return Math.abs(date1 - date2);
|
|
245
|
+
} catch {
|
|
246
|
+
return Infinity;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Add session to chain
|
|
252
|
+
* @param {string} sessionId - Session ID to add to
|
|
253
|
+
* @param {string} linkedSessionId - Session ID to link
|
|
254
|
+
* @param {string} position - Position ('before' or 'after')
|
|
255
|
+
* @returns {boolean} Success
|
|
256
|
+
*/
|
|
257
|
+
addToChain(sessionId, linkedSessionId, position = 'after') {
|
|
258
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
259
|
+
if (!session) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const linkedSession = this.sessionManager.loadSession(linkedSessionId);
|
|
264
|
+
if (!linkedSession) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let chain = session.metadata?.session_chain || [];
|
|
269
|
+
|
|
270
|
+
// Ensure current session is in chain
|
|
271
|
+
if (!chain.includes(sessionId)) {
|
|
272
|
+
chain.push(sessionId);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const currentIndex = chain.indexOf(sessionId);
|
|
276
|
+
|
|
277
|
+
if (position === 'after') {
|
|
278
|
+
// Insert after current
|
|
279
|
+
chain.splice(currentIndex + 1, 0, linkedSessionId);
|
|
280
|
+
} else if (position === 'before') {
|
|
281
|
+
// Insert before current
|
|
282
|
+
chain.splice(currentIndex, 0, linkedSessionId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update both sessions
|
|
286
|
+
this.sessionManager.updateSession(sessionId, {
|
|
287
|
+
metadata: { session_chain: chain }
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Also update linked session's chain
|
|
291
|
+
const linkedChain = linkedSession.metadata?.session_chain || [];
|
|
292
|
+
if (!linkedChain.includes(sessionId)) {
|
|
293
|
+
linkedChain.push(sessionId);
|
|
294
|
+
this.sessionManager.updateSession(linkedSessionId, {
|
|
295
|
+
metadata: { session_chain: linkedChain }
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
logger.info('Session added to chain', { sessionId, linkedSessionId, position });
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = SessionChain;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Error Classes
|
|
5
|
+
*
|
|
6
|
+
* Provides structured error handling for session operations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class SessionError extends Error {
|
|
10
|
+
constructor(message, options = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'SessionError';
|
|
13
|
+
this.code = options.code || 'SESSION_ERROR';
|
|
14
|
+
this.details = options.details || {};
|
|
15
|
+
this.timestamp = new Date().toISOString();
|
|
16
|
+
Error.captureStackTrace(this, SessionError);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toJSON() {
|
|
20
|
+
return {
|
|
21
|
+
name: this.name,
|
|
22
|
+
code: this.code,
|
|
23
|
+
message: this.message,
|
|
24
|
+
details: this.details,
|
|
25
|
+
timestamp: this.timestamp
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class SessionNotFoundError extends SessionError {
|
|
31
|
+
constructor(sessionId, options = {}) {
|
|
32
|
+
super(`Session '${sessionId}' not found`, {
|
|
33
|
+
code: 'SESSION_NOT_FOUND',
|
|
34
|
+
details: { sessionId, ...options.details }
|
|
35
|
+
});
|
|
36
|
+
this.name = 'SessionNotFoundError';
|
|
37
|
+
this.sessionId = sessionId;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class SessionChainError extends SessionError {
|
|
42
|
+
constructor(message, chain = [], options = {}) {
|
|
43
|
+
super(message, {
|
|
44
|
+
code: 'SESSION_CHAIN_ERROR',
|
|
45
|
+
details: { chain, ...options.details }
|
|
46
|
+
});
|
|
47
|
+
this.name = 'SessionChainError';
|
|
48
|
+
this.chain = chain;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class SessionExportError extends SessionError {
|
|
53
|
+
constructor(format, reason, options = {}) {
|
|
54
|
+
super(`Export failed for format '${format}': ${reason}`, {
|
|
55
|
+
code: 'SESSION_EXPORT_ERROR',
|
|
56
|
+
details: { format, reason, ...options.details }
|
|
57
|
+
});
|
|
58
|
+
this.name = 'SessionExportError';
|
|
59
|
+
this.format = format;
|
|
60
|
+
this.reason = reason;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class SessionImportError extends SessionError {
|
|
65
|
+
constructor(message, validationErrors = [], options = {}) {
|
|
66
|
+
super(message, {
|
|
67
|
+
code: 'SESSION_IMPORT_ERROR',
|
|
68
|
+
details: { validationErrors, ...options.details }
|
|
69
|
+
});
|
|
70
|
+
this.name = 'SessionImportError';
|
|
71
|
+
this.validationErrors = validationErrors;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
SessionError,
|
|
77
|
+
SessionNotFoundError,
|
|
78
|
+
SessionChainError,
|
|
79
|
+
SessionExportError,
|
|
80
|
+
SessionImportError
|
|
81
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Export — Export session data for model handoff
|
|
5
|
+
*
|
|
6
|
+
* Supports markdown and JSON export formats
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { SessionExportError, SessionNotFoundError } = require('./session-errors.cjs');
|
|
10
|
+
const { safePlanningWriteSync } = require('./planning-write.cjs');
|
|
11
|
+
|
|
12
|
+
class SessionExport {
|
|
13
|
+
/**
|
|
14
|
+
* Create a SessionExport instance
|
|
15
|
+
* @param {Object} sessionManager - SessionManager instance
|
|
16
|
+
*/
|
|
17
|
+
constructor(sessionManager) {
|
|
18
|
+
this.sessionManager = sessionManager;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Export a session in the specified format
|
|
23
|
+
* @param {string} sessionId - Session ID
|
|
24
|
+
* @param {string} format - Export format ('markdown' or 'json')
|
|
25
|
+
* @returns {Object} Export result with content and path
|
|
26
|
+
*/
|
|
27
|
+
export(sessionId, format = 'markdown') {
|
|
28
|
+
const session = this.sessionManager.loadSession(sessionId);
|
|
29
|
+
if (!session) {
|
|
30
|
+
throw new SessionNotFoundError(sessionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let content;
|
|
34
|
+
switch (format) {
|
|
35
|
+
case 'markdown':
|
|
36
|
+
content = this.toMarkdown(session);
|
|
37
|
+
break;
|
|
38
|
+
case 'json':
|
|
39
|
+
content = this.toJSON(session);
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
throw new SessionExportError(format, 'Unsupported format');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ext = format === 'markdown' ? 'md' : 'json';
|
|
46
|
+
const outputPath = `.planning/sessions/export-${sessionId}.${ext}`;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
format,
|
|
51
|
+
content,
|
|
52
|
+
outputPath
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert session to markdown format
|
|
58
|
+
* @param {Object} session - Session object
|
|
59
|
+
* @returns {string} Markdown string
|
|
60
|
+
*/
|
|
61
|
+
toMarkdown(session) {
|
|
62
|
+
const { metadata, context, state } = session;
|
|
63
|
+
const exportedAt = new Date().toISOString();
|
|
64
|
+
|
|
65
|
+
// Calculate duration
|
|
66
|
+
let duration = 'N/A';
|
|
67
|
+
if (metadata.started_at && metadata.ended_at) {
|
|
68
|
+
const start = new Date(metadata.started_at);
|
|
69
|
+
const end = new Date(metadata.ended_at);
|
|
70
|
+
const diffMs = end - start;
|
|
71
|
+
const diffHrs = Math.floor(diffMs / (1000 * 60 * 60));
|
|
72
|
+
const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
73
|
+
if (diffHrs > 0) {
|
|
74
|
+
duration = `${diffHrs}h ${diffMins}m`;
|
|
75
|
+
} else {
|
|
76
|
+
duration = `${diffMins}m`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build markdown
|
|
81
|
+
let md = `# Session Export: ${metadata.session_id}\n\n`;
|
|
82
|
+
md += `**Exported:** ${exportedAt}\n`;
|
|
83
|
+
md += `**Model:** ${metadata.model || 'Unknown'}\n`;
|
|
84
|
+
md += `**Phase:** ${metadata.phase || 'N/A'}\n`;
|
|
85
|
+
md += `**Plan:** ${metadata.plan || 'N/A'}\n`;
|
|
86
|
+
md += `**Duration:** ${duration}\n\n`;
|
|
87
|
+
md += `---\n\n`;
|
|
88
|
+
|
|
89
|
+
// Session Summary
|
|
90
|
+
md += `## Session Summary\n\n`;
|
|
91
|
+
md += `**Objective:** ${state.next_recommended_action || 'Not specified'}\n\n`;
|
|
92
|
+
|
|
93
|
+
md += `**Completed:**\n`;
|
|
94
|
+
if (context.tasks && context.tasks.length > 0) {
|
|
95
|
+
const completedTasks = context.tasks.filter(t => t.status === 'completed');
|
|
96
|
+
if (completedTasks.length > 0) {
|
|
97
|
+
completedTasks.forEach(t => md += `- ${t.name || t.description || 'Task'}\n`);
|
|
98
|
+
} else {
|
|
99
|
+
md += `- None\n`;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
md += `- None\n`;
|
|
103
|
+
}
|
|
104
|
+
md += `\n`;
|
|
105
|
+
|
|
106
|
+
md += `**Incomplete:**\n`;
|
|
107
|
+
if (state.incomplete_tasks && state.incomplete_tasks.length > 0) {
|
|
108
|
+
state.incomplete_tasks.forEach(t => md += `- ${t}\n`);
|
|
109
|
+
} else {
|
|
110
|
+
md += `- None\n`;
|
|
111
|
+
}
|
|
112
|
+
md += `\n`;
|
|
113
|
+
|
|
114
|
+
md += `---\n\n`;
|
|
115
|
+
|
|
116
|
+
// Key Decisions
|
|
117
|
+
md += `## Key Decisions\n\n`;
|
|
118
|
+
if (context.decisions && context.decisions.length > 0) {
|
|
119
|
+
context.decisions.forEach((decision, index) => {
|
|
120
|
+
md += `${index + 1}. **${decision.title || 'Decision'}**\n`;
|
|
121
|
+
if (decision.rationale) {
|
|
122
|
+
md += ` - Rationale: ${decision.rationale}\n`;
|
|
123
|
+
}
|
|
124
|
+
if (decision.status) {
|
|
125
|
+
md += ` - Status: ${decision.status}\n`;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
md += `No decisions recorded\n`;
|
|
130
|
+
}
|
|
131
|
+
md += `\n`;
|
|
132
|
+
|
|
133
|
+
md += `---\n\n`;
|
|
134
|
+
|
|
135
|
+
// File Changes
|
|
136
|
+
md += `## File Changes\n\n`;
|
|
137
|
+
if (context.file_changes && context.file_changes.length > 0) {
|
|
138
|
+
md += `| File | Action | Reason |\n`;
|
|
139
|
+
md += `|------|--------|--------|\n`;
|
|
140
|
+
context.file_changes.forEach(change => {
|
|
141
|
+
md += `| ${change.file || 'Unknown'} | ${change.action || 'modified'} | ${change.reason || '-'} |\n`;
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
md += `No file changes recorded\n`;
|
|
145
|
+
}
|
|
146
|
+
md += `\n`;
|
|
147
|
+
|
|
148
|
+
md += `---\n\n`;
|
|
149
|
+
|
|
150
|
+
// Open Questions
|
|
151
|
+
md += `## Open Questions\n\n`;
|
|
152
|
+
if (context.open_questions && context.open_questions.length > 0) {
|
|
153
|
+
context.open_questions.forEach(q => md += `- ${q}\n`);
|
|
154
|
+
} else {
|
|
155
|
+
md += `None\n`;
|
|
156
|
+
}
|
|
157
|
+
md += `\n`;
|
|
158
|
+
|
|
159
|
+
md += `---\n\n`;
|
|
160
|
+
|
|
161
|
+
// Blockers
|
|
162
|
+
md += `## Blockers/Concerns\n\n`;
|
|
163
|
+
if (context.blockers && context.blockers.length > 0) {
|
|
164
|
+
context.blockers.forEach(b => md += `- ${b}\n`);
|
|
165
|
+
} else {
|
|
166
|
+
md += `None\n`;
|
|
167
|
+
}
|
|
168
|
+
md += `\n`;
|
|
169
|
+
|
|
170
|
+
md += `---\n\n`;
|
|
171
|
+
|
|
172
|
+
// Recommended Next Actions
|
|
173
|
+
md += `## Recommended Next Actions\n\n`;
|
|
174
|
+
if (state.next_recommended_action) {
|
|
175
|
+
md += `- ${state.next_recommended_action}\n`;
|
|
176
|
+
} else {
|
|
177
|
+
md += `None\n`;
|
|
178
|
+
}
|
|
179
|
+
md += `\n`;
|
|
180
|
+
|
|
181
|
+
md += `---\n\n`;
|
|
182
|
+
|
|
183
|
+
// Session Chain
|
|
184
|
+
md += `## Session Chain\n\n`;
|
|
185
|
+
const chain = metadata.session_chain || [];
|
|
186
|
+
const currentIndex = chain.indexOf(metadata.session_id);
|
|
187
|
+
|
|
188
|
+
if (chain.length > 0) {
|
|
189
|
+
md += `- Previous: ${currentIndex > 0 ? chain[currentIndex - 1] : 'none'}\n`;
|
|
190
|
+
md += `- Current: ${metadata.session_id}\n`;
|
|
191
|
+
md += `- Next: ${currentIndex < chain.length - 1 ? chain[currentIndex + 1] : 'none'}\n`;
|
|
192
|
+
} else {
|
|
193
|
+
md += `- Previous: none\n`;
|
|
194
|
+
md += `- Current: ${metadata.session_id}\n`;
|
|
195
|
+
md += `- Next: none\n`;
|
|
196
|
+
}
|
|
197
|
+
md += `\n`;
|
|
198
|
+
|
|
199
|
+
// Token Usage (if available)
|
|
200
|
+
if (metadata.token_usage && (metadata.token_usage.input || metadata.token_usage.output)) {
|
|
201
|
+
md += `---\n\n`;
|
|
202
|
+
md += `## Token Usage\n\n`;
|
|
203
|
+
md += `- Input: ${metadata.token_usage.input || 0}\n`;
|
|
204
|
+
md += `- Output: ${metadata.token_usage.output || 0}\n`;
|
|
205
|
+
md += `- Total: ${metadata.token_usage.total || 0}\n`;
|
|
206
|
+
md += `\n`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return md;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Convert session to JSON format
|
|
214
|
+
* @param {Object} session - Session object
|
|
215
|
+
* @returns {string} JSON string
|
|
216
|
+
*/
|
|
217
|
+
toJSON(session) {
|
|
218
|
+
const exportData = {
|
|
219
|
+
export_version: '1.0',
|
|
220
|
+
exported_at: new Date().toISOString(),
|
|
221
|
+
export_format: 'json',
|
|
222
|
+
session: session
|
|
223
|
+
};
|
|
224
|
+
return JSON.stringify(exportData, null, 2);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Export session to file
|
|
229
|
+
* @param {string} sessionId - Session ID
|
|
230
|
+
* @param {string} format - Export format
|
|
231
|
+
* @param {string} outputPath - Output file path
|
|
232
|
+
* @returns {Object} Export result with path
|
|
233
|
+
*/
|
|
234
|
+
exportToFile(sessionId, format, outputPath) {
|
|
235
|
+
const result = this.export(sessionId, format);
|
|
236
|
+
|
|
237
|
+
if (!outputPath) {
|
|
238
|
+
outputPath = result.outputPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
safePlanningWriteSync(outputPath, result.content);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
success: true,
|
|
245
|
+
path: outputPath,
|
|
246
|
+
format
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = SessionExport;
|