clearctx 3.0.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.
@@ -0,0 +1,473 @@
1
+ /**
2
+ * diff-engine.js
3
+ *
4
+ * The Diff Engine computes what changed between sessions.
5
+ * This is Layer 0 of the Session Continuity Engine.
6
+ *
7
+ * Primary use case: "What changed since my last session?"
8
+ * - Compares latest snapshot to current git state
9
+ * - Detects new commits, file changes, and stale context
10
+ * - Provides time-aware session continuity
11
+ *
12
+ * Uses synchronous git commands and zero external dependencies.
13
+ */
14
+
15
+ const { execSync } = require('child_process');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const crypto = require('crypto');
19
+ const SessionSnapshot = require('./session-snapshot');
20
+
21
+ /**
22
+ * DiffEngine - Computes differences between snapshots and current state
23
+ *
24
+ * This class analyzes what changed between sessions by comparing
25
+ * git state, file modifications, and working context.
26
+ */
27
+ class DiffEngine {
28
+ /**
29
+ * Create a new DiffEngine
30
+ * @param {string} projectPath - Absolute path to the project directory
31
+ */
32
+ constructor(projectPath) {
33
+ // Store the project path for git operations
34
+ this.projectPath = projectPath;
35
+
36
+ // Create a SessionSnapshot instance to read snapshots
37
+ this.snapshot = new SessionSnapshot(projectPath);
38
+ }
39
+
40
+ /**
41
+ * Compare latest snapshot to current git state
42
+ *
43
+ * This is the PRIMARY use case — answers "what changed since my last session?"
44
+ *
45
+ * @returns {Object} Diff object with git changes, file changes, and context impact
46
+ */
47
+ diffFromLatest() {
48
+ // Step 1: Get the latest snapshot
49
+ const snapshot = this.snapshot.getLatest();
50
+
51
+ // If no snapshot exists, this is the first session
52
+ if (!snapshot) {
53
+ return {
54
+ noSnapshot: true,
55
+ message: 'No previous snapshot found. This appears to be the first session.'
56
+ };
57
+ }
58
+
59
+ // Step 2: Get the snapshot's git commit
60
+ const snapshotHead = snapshot.git?.headCommit || null;
61
+
62
+ // If snapshot has no git state (repo had no commits), return a special diff
63
+ if (!snapshotHead) {
64
+ return {
65
+ noGitState: true,
66
+ message: 'Previous snapshot had no git state (repository had no commits at that time).',
67
+ fromSnapshot: snapshot.snapshotId,
68
+ toState: 'current',
69
+ timeDelta: Date.now() - new Date(snapshot.capturedAt).getTime(),
70
+ timeDeltaHuman: this._formatTimeDelta(Date.now() - new Date(snapshot.capturedAt).getTime()),
71
+ gitChanges: {
72
+ newCommitCount: 0,
73
+ commits: [],
74
+ branchChanged: false,
75
+ oldBranch: null,
76
+ newBranch: null,
77
+ diffFailed: false
78
+ },
79
+ fileChanges: {
80
+ added: [],
81
+ modified: [],
82
+ deleted: [],
83
+ renamed: [],
84
+ diffFailed: false
85
+ },
86
+ contextImpact: {
87
+ activeFilesModified: [],
88
+ staleFiles: [],
89
+ previousTask: snapshot.workingContext?.taskSummary || '',
90
+ previousQuestions: snapshot.workingContext?.openQuestions || []
91
+ }
92
+ };
93
+ }
94
+
95
+ // Step 3: Compute current git state
96
+ let currentHead, currentBranch;
97
+ try {
98
+ // Get current commit hash
99
+ currentHead = execSync('git rev-parse HEAD', {
100
+ cwd: this.projectPath,
101
+ encoding: 'utf8'
102
+ }).trim();
103
+
104
+ // Get current branch name
105
+ currentBranch = execSync('git branch --show-current', {
106
+ cwd: this.projectPath,
107
+ encoding: 'utf8'
108
+ }).trim();
109
+ } catch (err) {
110
+ return {
111
+ error: true,
112
+ message: 'Failed to get current git state: ' + err.message
113
+ };
114
+ }
115
+
116
+ // Step 4: Compute git changes between snapshot and now
117
+ let commits = [];
118
+ let commitDiffFailed = false;
119
+
120
+ try {
121
+ // Get new commits since snapshot
122
+ const logOutput = execSync(`git log --oneline ${snapshotHead}..HEAD`, {
123
+ cwd: this.projectPath,
124
+ encoding: 'utf8'
125
+ }).trim();
126
+
127
+ if (logOutput) {
128
+ commits = this._parseOneline(logOutput);
129
+ }
130
+ } catch (err) {
131
+ // Snapshot commit might not exist (rebased/force-pushed)
132
+ commitDiffFailed = true;
133
+ commits = [{
134
+ hash: 'N/A',
135
+ message: `Cannot compute commits (snapshot commit ${snapshotHead.slice(0, 7)} no longer exists)`
136
+ }];
137
+ }
138
+
139
+ // Step 5: Compute file changes
140
+ let fileChanges = { added: [], modified: [], deleted: [], renamed: [] };
141
+ let fileDiffFailed = false;
142
+
143
+ try {
144
+ if (!commitDiffFailed && snapshotHead !== currentHead) {
145
+ // Use git diff to find changes between commits
146
+ const diffOutput = execSync(`git diff --name-status ${snapshotHead} HEAD`, {
147
+ cwd: this.projectPath,
148
+ encoding: 'utf8'
149
+ }).trim();
150
+
151
+ if (diffOutput) {
152
+ fileChanges = this._parseNameStatus(diffOutput);
153
+ }
154
+ } else if (commitDiffFailed) {
155
+ // Fallback: use git status if snapshot commit doesn't exist
156
+ const statusOutput = execSync('git status --porcelain', {
157
+ cwd: this.projectPath,
158
+ encoding: 'utf8'
159
+ }).trim();
160
+
161
+ if (statusOutput) {
162
+ fileChanges = this._parseStatusPorcelain(statusOutput);
163
+ }
164
+ }
165
+ // If snapshotHead === currentHead, no file changes (already initialized empty)
166
+ } catch (err) {
167
+ fileDiffFailed = true;
168
+ }
169
+
170
+ // Step 6: Compute context impact
171
+ const activeFiles = snapshot.workingContext?.activeFiles || [];
172
+ const allChangedFiles = [
173
+ ...fileChanges.added,
174
+ ...fileChanges.modified,
175
+ ...fileChanges.deleted,
176
+ ...fileChanges.renamed.map(r => r.from),
177
+ ...fileChanges.renamed.map(r => r.to)
178
+ ];
179
+
180
+ // Find which active files were modified
181
+ const activeFilesModified = activeFiles.filter(file =>
182
+ allChangedFiles.includes(file)
183
+ );
184
+
185
+ // Step 7: Compute time delta
186
+ const timeDeltaMs = Date.now() - new Date(snapshot.capturedAt).getTime();
187
+
188
+ // Step 8: Return comprehensive diff object
189
+ return {
190
+ fromSnapshot: snapshot.snapshotId,
191
+ toState: 'current',
192
+ timeDelta: timeDeltaMs,
193
+ timeDeltaHuman: this._formatTimeDelta(timeDeltaMs),
194
+ gitChanges: {
195
+ newCommitCount: commits.length,
196
+ commits: commits,
197
+ branchChanged: snapshot.git?.branch !== currentBranch,
198
+ oldBranch: snapshot.git?.branch || 'unknown',
199
+ newBranch: currentBranch,
200
+ diffFailed: commitDiffFailed
201
+ },
202
+ fileChanges: {
203
+ added: fileChanges.added,
204
+ modified: fileChanges.modified,
205
+ deleted: fileChanges.deleted,
206
+ renamed: fileChanges.renamed,
207
+ diffFailed: fileDiffFailed
208
+ },
209
+ contextImpact: {
210
+ activeFilesModified: activeFilesModified,
211
+ staleFiles: activeFilesModified, // Alias for clarity
212
+ previousTask: snapshot.workingContext?.taskSummary || '',
213
+ previousQuestions: snapshot.workingContext?.openQuestions || []
214
+ }
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Compare two specific snapshots
220
+ *
221
+ * @param {string} snapshotIdA - First snapshot ID
222
+ * @param {string} snapshotIdB - Second snapshot ID
223
+ * @returns {Object} Diff object between the two snapshots
224
+ */
225
+ diffBetween(snapshotIdA, snapshotIdB) {
226
+ // Step 1: Get both snapshots
227
+ const snapshotA = this.snapshot.get(snapshotIdA);
228
+ const snapshotB = this.snapshot.get(snapshotIdB);
229
+
230
+ if (!snapshotA) {
231
+ throw new Error(`Snapshot not found: ${snapshotIdA}`);
232
+ }
233
+ if (!snapshotB) {
234
+ throw new Error(`Snapshot not found: ${snapshotIdB}`);
235
+ }
236
+
237
+ // Step 2: Get git commits from both snapshots
238
+ const commitA = snapshotA.git?.headCommit;
239
+ const commitB = snapshotB.git?.headCommit;
240
+
241
+ if (!commitA || !commitB) {
242
+ throw new Error('One or both snapshots missing git headCommit');
243
+ }
244
+
245
+ // Step 3: Compute commit list
246
+ let commits = [];
247
+ let commitDiffFailed = false;
248
+
249
+ try {
250
+ const logOutput = execSync(`git log --oneline ${commitA}..${commitB}`, {
251
+ cwd: this.projectPath,
252
+ encoding: 'utf8'
253
+ }).trim();
254
+
255
+ if (logOutput) {
256
+ commits = this._parseOneline(logOutput);
257
+ }
258
+ } catch (err) {
259
+ commitDiffFailed = true;
260
+ commits = [{
261
+ hash: 'N/A',
262
+ message: 'Cannot compute commits (one or both commits no longer exist)'
263
+ }];
264
+ }
265
+
266
+ // Step 4: Compute file changes
267
+ let fileChanges = { added: [], modified: [], deleted: [], renamed: [] };
268
+ let fileDiffFailed = false;
269
+
270
+ try {
271
+ if (!commitDiffFailed) {
272
+ const diffOutput = execSync(`git diff --name-status ${commitA} ${commitB}`, {
273
+ cwd: this.projectPath,
274
+ encoding: 'utf8'
275
+ }).trim();
276
+
277
+ if (diffOutput) {
278
+ fileChanges = this._parseNameStatus(diffOutput);
279
+ }
280
+ }
281
+ } catch (err) {
282
+ fileDiffFailed = true;
283
+ }
284
+
285
+ // Step 5: Compute time delta
286
+ const timeDeltaMs = new Date(snapshotB.capturedAt).getTime() -
287
+ new Date(snapshotA.capturedAt).getTime();
288
+
289
+ // Step 6: Return diff object
290
+ return {
291
+ fromSnapshot: snapshotIdA,
292
+ toSnapshot: snapshotIdB,
293
+ timeDelta: timeDeltaMs,
294
+ timeDeltaHuman: this._formatTimeDelta(timeDeltaMs),
295
+ gitChanges: {
296
+ newCommitCount: commits.length,
297
+ commits: commits,
298
+ branchChanged: snapshotA.git?.branch !== snapshotB.git?.branch,
299
+ oldBranch: snapshotA.git?.branch || 'unknown',
300
+ newBranch: snapshotB.git?.branch || 'unknown',
301
+ diffFailed: commitDiffFailed
302
+ },
303
+ fileChanges: {
304
+ added: fileChanges.added,
305
+ modified: fileChanges.modified,
306
+ deleted: fileChanges.deleted,
307
+ renamed: fileChanges.renamed,
308
+ diffFailed: fileDiffFailed
309
+ },
310
+ contextImpact: {
311
+ activeFilesModified: [], // Not computed for snapshot-to-snapshot diff
312
+ staleFiles: [],
313
+ previousTask: snapshotA.workingContext?.taskSummary || '',
314
+ previousQuestions: snapshotA.workingContext?.openQuestions || []
315
+ }
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Parse git diff --name-status output
321
+ *
322
+ * Format examples:
323
+ * A src/new-file.ts → added
324
+ * M src/modified.ts → modified
325
+ * D src/deleted.ts → deleted
326
+ * R100 src/old.ts src/new.ts → renamed
327
+ *
328
+ * @param {string} output - Output from git diff --name-status
329
+ * @returns {Object} Categorized file changes
330
+ * @private
331
+ */
332
+ _parseNameStatus(output) {
333
+ const result = {
334
+ added: [],
335
+ modified: [],
336
+ deleted: [],
337
+ renamed: []
338
+ };
339
+
340
+ if (!output) return result;
341
+
342
+ const lines = output.split('\n');
343
+ for (const line of lines) {
344
+ if (!line.trim()) continue;
345
+
346
+ // Split by tabs or multiple spaces
347
+ const parts = line.split(/\s+/);
348
+ const status = parts[0];
349
+
350
+ if (status.startsWith('A')) {
351
+ // Added file
352
+ result.added.push(parts[1]);
353
+ } else if (status.startsWith('M')) {
354
+ // Modified file
355
+ result.modified.push(parts[1]);
356
+ } else if (status.startsWith('D')) {
357
+ // Deleted file
358
+ result.deleted.push(parts[1]);
359
+ } else if (status.startsWith('R')) {
360
+ // Renamed file (R100 means 100% similarity)
361
+ result.renamed.push({
362
+ from: parts[1],
363
+ to: parts[2]
364
+ });
365
+ }
366
+ }
367
+
368
+ return result;
369
+ }
370
+
371
+ /**
372
+ * Parse git status --porcelain output (fallback when diff fails)
373
+ *
374
+ * Format examples:
375
+ * ?? new-file.ts → added (untracked)
376
+ * M modified.ts → modified
377
+ * D deleted.ts → deleted
378
+ *
379
+ * @param {string} output - Output from git status --porcelain
380
+ * @returns {Object} Categorized file changes
381
+ * @private
382
+ */
383
+ _parseStatusPorcelain(output) {
384
+ const result = {
385
+ added: [],
386
+ modified: [],
387
+ deleted: [],
388
+ renamed: []
389
+ };
390
+
391
+ if (!output) return result;
392
+
393
+ const lines = output.split('\n');
394
+ for (const line of lines) {
395
+ if (!line.trim()) continue;
396
+
397
+ // First two characters are status codes
398
+ const status = line.substring(0, 2);
399
+ const file = line.substring(3).trim();
400
+
401
+ if (status.includes('?')) {
402
+ result.added.push(file);
403
+ } else if (status.includes('M') || status.includes('A')) {
404
+ result.modified.push(file);
405
+ } else if (status.includes('D')) {
406
+ result.deleted.push(file);
407
+ }
408
+ }
409
+
410
+ return result;
411
+ }
412
+
413
+ /**
414
+ * Parse git log --oneline output
415
+ *
416
+ * Format: "abc123d Some commit message"
417
+ *
418
+ * @param {string} output - Output from git log --oneline
419
+ * @returns {Array<Object>} Array of {hash, message} objects
420
+ * @private
421
+ */
422
+ _parseOneline(output) {
423
+ const commits = [];
424
+
425
+ if (!output) return commits;
426
+
427
+ const lines = output.split('\n');
428
+ for (const line of lines) {
429
+ if (!line.trim()) continue;
430
+
431
+ // First word is the hash, rest is the message
432
+ const spaceIndex = line.indexOf(' ');
433
+ if (spaceIndex === -1) continue;
434
+
435
+ const hash = line.substring(0, spaceIndex);
436
+ const message = line.substring(spaceIndex + 1);
437
+
438
+ commits.push({ hash, message });
439
+ }
440
+
441
+ return commits;
442
+ }
443
+
444
+ /**
445
+ * Convert milliseconds to human-readable time string
446
+ *
447
+ * @param {number} ms - Milliseconds
448
+ * @returns {string} Human-readable time (e.g., "5 minutes ago")
449
+ * @private
450
+ */
451
+ _formatTimeDelta(ms) {
452
+ const seconds = Math.floor(ms / 1000);
453
+ const minutes = Math.floor(seconds / 60);
454
+ const hours = Math.floor(minutes / 60);
455
+ const days = Math.floor(hours / 24);
456
+ const weeks = Math.floor(days / 7);
457
+
458
+ if (seconds < 60) {
459
+ return 'just now';
460
+ } else if (minutes < 60) {
461
+ return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
462
+ } else if (hours < 24) {
463
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
464
+ } else if (days < 7) {
465
+ return `${days} day${days === 1 ? '' : 's'} ago`;
466
+ } else {
467
+ return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
468
+ }
469
+ }
470
+ }
471
+
472
+ // Export the DiffEngine class directly
473
+ module.exports = DiffEngine;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Cross-process file locking with PID-aware stale lock recovery.
3
+ *
4
+ * Uses exclusive file creation (wx flag) for atomic lock acquisition.
5
+ * Handles stale locks from crashed processes by checking PID liveness.
6
+ *
7
+ * Lock stealing rules:
8
+ * 1. If lock owner PID is dead → steal immediately
9
+ * 2. If lock owner PID is alive but lock is stale → steal after timeout
10
+ * 3. If lock owner PID is alive and lock is fresh → retry until timeout
11
+ *
12
+ * Configuration via environment variables:
13
+ * - CMS_LOCK_STALE_MS: Milliseconds before a lock is considered stale (default: 30000)
14
+ * - CMS_LOCK_MAX_RETRIES: Max retry attempts (default: 200)
15
+ * - CMS_LOCK_INTERVAL_MS: Wait time between retries (default: 50)
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ /**
22
+ * Acquire an exclusive lock using atomic file creation.
23
+ * Retries with backoff if lock is held. Steals stale locks.
24
+ *
25
+ * @param {string} lockDir - Directory for lock files
26
+ * @param {string} lockName - Name of lock (e.g., 'session-foo')
27
+ * @param {object} [options={}] - Lock options
28
+ * @param {number} [options.staleMs] - Lock staleness timeout (default: 30000)
29
+ * @param {number} [options.maxRetries] - Max acquisition attempts (default: 200)
30
+ * @param {number} [options.retryIntervalMs] - Wait between retries (default: 50)
31
+ * @throws {Error} Throws if lock cannot be acquired within timeout
32
+ */
33
+ function acquireLock(lockDir, lockName, options = {}) {
34
+ // Configuration with env var overrides
35
+ const staleMs = options.staleMs || Number(process.env.CMS_LOCK_STALE_MS) || 30000;
36
+ const maxRetries = options.maxRetries || Number(process.env.CMS_LOCK_MAX_RETRIES) || 200;
37
+ const retryIntervalMs = options.retryIntervalMs || Number(process.env.CMS_LOCK_INTERVAL_MS) || 50;
38
+
39
+ const lockFile = path.join(lockDir, `${lockName}.lock`);
40
+
41
+ // Ensure lock directory exists
42
+ if (!fs.existsSync(lockDir)) {
43
+ fs.mkdirSync(lockDir, { recursive: true });
44
+ }
45
+
46
+ // Lock payload: PID for ownership verification, timestamp for staleness check
47
+ const lockData = {
48
+ pid: process.pid,
49
+ timestamp: Date.now()
50
+ };
51
+
52
+ // Retry loop with exponential backoff
53
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
54
+ try {
55
+ // Try to create lock file exclusively (atomic operation)
56
+ fs.writeFileSync(lockFile, JSON.stringify(lockData), { flag: 'wx' });
57
+ return; // Success! Lock acquired
58
+ } catch (err) {
59
+ if (err.code !== 'EEXIST') {
60
+ // Unexpected error (permission denied, disk full, etc.)
61
+ throw err;
62
+ }
63
+
64
+ // Lock file exists — check if we can steal it
65
+ try {
66
+ const existingLock = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
67
+ const ownerPid = existingLock.pid;
68
+ const lockTimestamp = existingLock.timestamp;
69
+
70
+ // Check if owner process is still alive
71
+ let isAlive = false;
72
+ try {
73
+ // process.kill(pid, 0) throws if process doesn't exist
74
+ // Signal 0 = no signal sent, just checks existence
75
+ process.kill(ownerPid, 0);
76
+ isAlive = true;
77
+ } catch (killErr) {
78
+ // Process is dead (ESRCH = no such process)
79
+ isAlive = false;
80
+ }
81
+
82
+ if (!isAlive) {
83
+ // Owner process is dead — steal the lock immediately
84
+ try {
85
+ fs.unlinkSync(lockFile);
86
+ } catch (unlinkErr) {
87
+ // Lock might have been deleted by another process — retry
88
+ }
89
+ continue; // Retry immediately
90
+ }
91
+
92
+ // Owner process is alive — check if lock is stale
93
+ const lockAge = Date.now() - lockTimestamp;
94
+ if (lockAge > staleMs) {
95
+ // Lock is held too long — assume deadlock, steal it
96
+ try {
97
+ fs.unlinkSync(lockFile);
98
+ } catch (unlinkErr) {
99
+ // Lock might have been deleted by another process — retry
100
+ }
101
+ continue; // Retry immediately
102
+ }
103
+
104
+ // Lock is fresh and owner is alive — wait and retry
105
+ } catch (readErr) {
106
+ // Lock file is corrupt or deleted — retry immediately
107
+ continue;
108
+ }
109
+ }
110
+
111
+ // Wait before retrying (synchronous sleep using Atomics)
112
+ // Creates a shared buffer and waits on it (will always timeout)
113
+ const sharedBuffer = new SharedArrayBuffer(4);
114
+ const sharedArray = new Int32Array(sharedBuffer);
115
+ Atomics.wait(sharedArray, 0, 0, retryIntervalMs);
116
+ }
117
+
118
+ // Failed to acquire lock within timeout
119
+ throw new Error(`Lock acquisition timeout for ${lockName} after ${maxRetries} retries`);
120
+ }
121
+
122
+ /**
123
+ * Release a lock if owned by current process.
124
+ * Verifies ownership by checking PID before deletion.
125
+ *
126
+ * @param {string} lockDir - Directory for lock files
127
+ * @param {string} lockName - Name of lock to release
128
+ */
129
+ function releaseLock(lockDir, lockName) {
130
+ const lockFile = path.join(lockDir, `${lockName}.lock`);
131
+
132
+ // Check if lock file exists
133
+ if (!fs.existsSync(lockFile)) {
134
+ // Already released or never acquired — success
135
+ return;
136
+ }
137
+
138
+ try {
139
+ // Read lock to verify ownership
140
+ const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
141
+
142
+ if (lockData.pid === process.pid) {
143
+ // We own this lock — safe to delete
144
+ fs.unlinkSync(lockFile);
145
+ } else {
146
+ // Someone else owns this lock (stolen from us?)
147
+ console.error(
148
+ `WARNING: Lock ${lockName} is owned by PID ${lockData.pid}, not ${process.pid}. ` +
149
+ `Not releasing. This may indicate a stolen lock or timing issue.`
150
+ );
151
+ }
152
+ } catch (err) {
153
+ // Lock file disappeared or is corrupt — consider it released
154
+ return;
155
+ }
156
+ }
157
+
158
+ module.exports = {
159
+ acquireLock,
160
+ releaseLock
161
+ };
package/src/index.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * clearctx — Main exports
3
+ *
4
+ * Programmatic API for managing multiple Claude Code sessions.
5
+ *
6
+ * Usage:
7
+ * const { SessionManager, Delegate, StreamSession, SafetyNet } = require('clearctx');
8
+ *
9
+ * // High-level: delegate tasks with safety and control
10
+ * const mgr = new SessionManager();
11
+ * const delegate = new Delegate(mgr);
12
+ * const result = await delegate.run('fix-bug', { task: 'Fix the auth bug', maxCost: 0.50 });
13
+ *
14
+ * // Mid-level: manage sessions directly
15
+ * const { response } = await mgr.spawn('my-task', { prompt: 'Fix the auth bug' });
16
+ *
17
+ * // Low-level: single streaming session
18
+ * const session = new StreamSession({ name: 'direct', model: 'haiku' });
19
+ * session.start();
20
+ * const r = await session.send('Hello');
21
+ */
22
+
23
+ const SessionManager = require('./manager');
24
+ const StreamSession = require('./stream-session');
25
+ const Store = require('./store');
26
+ const Delegate = require('./delegate');
27
+ const SafetyNet = require('./safety-net');
28
+ const TeamHub = require('./team-hub');
29
+ const ArtifactStore = require('./artifact-store');
30
+ const ContractStore = require('./contract-store');
31
+ const DependencyResolver = require('./dependency-resolver');
32
+ const LineageGraph = require('./lineage-graph');
33
+ const PipelineEngine = require('./pipeline-engine');
34
+ const SnapshotEngine = require('./snapshot-engine');
35
+ const SessionSnapshot = require('./session-snapshot');
36
+ const DiffEngine = require('./diff-engine');
37
+ const BriefingGenerator = require('./briefing-generator');
38
+ const DecisionJournal = require('./decision-journal');
39
+ const PatternRegistry = require('./pattern-registry');
40
+ const StaleDetector = require('./stale-detector');
41
+
42
+ module.exports = {
43
+ SessionManager,
44
+ StreamSession,
45
+ Store,
46
+ Delegate,
47
+ SafetyNet,
48
+ TeamHub,
49
+ ArtifactStore,
50
+ ContractStore,
51
+ DependencyResolver,
52
+ LineageGraph,
53
+ PipelineEngine,
54
+ SnapshotEngine,
55
+ SessionSnapshot,
56
+ DiffEngine,
57
+ BriefingGenerator,
58
+ DecisionJournal,
59
+ PatternRegistry,
60
+ StaleDetector,
61
+ };