neuronlayer 0.1.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 (78) hide show
  1. package/CONTRIBUTING.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/index.js +38016 -0
  5. package/esbuild.config.js +26 -0
  6. package/package.json +63 -0
  7. package/src/cli/commands.ts +382 -0
  8. package/src/core/adr-exporter.ts +253 -0
  9. package/src/core/architecture/architecture-enforcement.ts +228 -0
  10. package/src/core/architecture/duplicate-detector.ts +288 -0
  11. package/src/core/architecture/index.ts +6 -0
  12. package/src/core/architecture/pattern-learner.ts +306 -0
  13. package/src/core/architecture/pattern-library.ts +403 -0
  14. package/src/core/architecture/pattern-validator.ts +324 -0
  15. package/src/core/change-intelligence/bug-correlator.ts +444 -0
  16. package/src/core/change-intelligence/change-intelligence.ts +221 -0
  17. package/src/core/change-intelligence/change-tracker.ts +334 -0
  18. package/src/core/change-intelligence/fix-suggester.ts +340 -0
  19. package/src/core/change-intelligence/index.ts +5 -0
  20. package/src/core/code-verifier.ts +843 -0
  21. package/src/core/confidence/confidence-scorer.ts +251 -0
  22. package/src/core/confidence/conflict-checker.ts +289 -0
  23. package/src/core/confidence/index.ts +5 -0
  24. package/src/core/confidence/source-tracker.ts +263 -0
  25. package/src/core/confidence/warning-detector.ts +241 -0
  26. package/src/core/context-rot/compaction.ts +284 -0
  27. package/src/core/context-rot/context-health.ts +243 -0
  28. package/src/core/context-rot/context-rot-prevention.ts +213 -0
  29. package/src/core/context-rot/critical-context.ts +221 -0
  30. package/src/core/context-rot/drift-detector.ts +255 -0
  31. package/src/core/context-rot/index.ts +7 -0
  32. package/src/core/context.ts +263 -0
  33. package/src/core/decision-extractor.ts +339 -0
  34. package/src/core/decisions.ts +69 -0
  35. package/src/core/deja-vu.ts +421 -0
  36. package/src/core/engine.ts +1455 -0
  37. package/src/core/feature-context.ts +726 -0
  38. package/src/core/ghost-mode.ts +412 -0
  39. package/src/core/learning.ts +485 -0
  40. package/src/core/living-docs/activity-tracker.ts +296 -0
  41. package/src/core/living-docs/architecture-generator.ts +428 -0
  42. package/src/core/living-docs/changelog-generator.ts +348 -0
  43. package/src/core/living-docs/component-generator.ts +230 -0
  44. package/src/core/living-docs/doc-engine.ts +110 -0
  45. package/src/core/living-docs/doc-validator.ts +282 -0
  46. package/src/core/living-docs/index.ts +8 -0
  47. package/src/core/project-manager.ts +297 -0
  48. package/src/core/summarizer.ts +267 -0
  49. package/src/core/test-awareness/change-validator.ts +499 -0
  50. package/src/core/test-awareness/index.ts +5 -0
  51. package/src/index.ts +49 -0
  52. package/src/indexing/ast.ts +563 -0
  53. package/src/indexing/embeddings.ts +85 -0
  54. package/src/indexing/indexer.ts +245 -0
  55. package/src/indexing/watcher.ts +78 -0
  56. package/src/server/gateways/aggregator.ts +374 -0
  57. package/src/server/gateways/index.ts +473 -0
  58. package/src/server/gateways/memory-ghost.ts +343 -0
  59. package/src/server/gateways/memory-query.ts +452 -0
  60. package/src/server/gateways/memory-record.ts +346 -0
  61. package/src/server/gateways/memory-review.ts +410 -0
  62. package/src/server/gateways/memory-status.ts +517 -0
  63. package/src/server/gateways/memory-verify.ts +392 -0
  64. package/src/server/gateways/router.ts +434 -0
  65. package/src/server/gateways/types.ts +610 -0
  66. package/src/server/mcp.ts +154 -0
  67. package/src/server/resources.ts +85 -0
  68. package/src/server/tools.ts +2261 -0
  69. package/src/storage/database.ts +262 -0
  70. package/src/storage/tier1.ts +135 -0
  71. package/src/storage/tier2.ts +764 -0
  72. package/src/storage/tier3.ts +123 -0
  73. package/src/types/documentation.ts +619 -0
  74. package/src/types/index.ts +222 -0
  75. package/src/utils/config.ts +193 -0
  76. package/src/utils/files.ts +117 -0
  77. package/src/utils/time.ts +37 -0
  78. package/src/utils/tokens.ts +52 -0
@@ -0,0 +1,726 @@
1
+ import { EventEmitter } from 'events';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
+ import { dirname, join, basename } from 'path';
4
+ import { randomUUID } from 'crypto';
5
+ import type { ActiveFeatureContext, FeatureFile, FeatureChange, FeatureQuery, HotContext } from '../types/index.js';
6
+
7
+ /**
8
+ * Context Resurrection - Restore mental state from last session
9
+ * "Last time you worked on auth, you were stuck on X"
10
+ */
11
+ export interface ResurrectedContext {
12
+ /** What files were you working on? */
13
+ activeFiles: string[];
14
+ /** What were you trying to do? */
15
+ lastQueries: string[];
16
+ /** What decisions were made during this context? */
17
+ sessionDecisions: string[];
18
+ /** Where did you leave off? */
19
+ lastEditedFile: string | null;
20
+ lastEditTime: Date | null;
21
+ /** What was the blocker (if any)? */
22
+ possibleBlocker: string | null;
23
+ /** Suggested next steps */
24
+ suggestedActions: string[];
25
+ /** Context summary for AI */
26
+ summary: string;
27
+ /** Time since last activity */
28
+ timeSinceLastActive: string;
29
+ }
30
+
31
+ export interface ContextResurrectionOptions {
32
+ /** Specific feature name to resurrect */
33
+ featureName?: string;
34
+ /** Include file contents in resurrection */
35
+ includeFileContents?: boolean;
36
+ /** Maximum files to include */
37
+ maxFiles?: number;
38
+ }
39
+
40
+ const MAX_FILES = 20;
41
+ const MAX_CHANGES = 50;
42
+ const MAX_QUERIES = 20;
43
+ const MAX_RECENT_CONTEXTS = 5;
44
+ const TTL_MINUTES = 30;
45
+ const HOT_CACHE_MAX_FILES = 15;
46
+
47
+ export class FeatureContextManager extends EventEmitter {
48
+ private current: ActiveFeatureContext | null = null;
49
+ private recent: ActiveFeatureContext[] = [];
50
+ private fileContents: Map<string, string> = new Map();
51
+ private projectPath: string;
52
+ private dataDir: string;
53
+ private persistPath: string;
54
+ private inactivityTimer: NodeJS.Timeout | null = null;
55
+
56
+ constructor(projectPath: string, dataDir: string) {
57
+ super();
58
+ this.projectPath = projectPath;
59
+ this.dataDir = dataDir;
60
+ this.persistPath = join(dataDir, 'feature-context.json');
61
+ this.load();
62
+ this.startInactivityTimer();
63
+ }
64
+
65
+ // ========== FILE TRACKING ==========
66
+
67
+ onFileOpened(filePath: string): void {
68
+ this.ensureContext();
69
+ this.touchFile(filePath);
70
+ this.preloadFile(filePath);
71
+ }
72
+
73
+ onFileEdited(filePath: string, diff: string, linesChanged: number[] = []): void {
74
+ this.ensureContext();
75
+ this.touchFile(filePath);
76
+ this.recordChange(filePath, diff, linesChanged);
77
+ }
78
+
79
+ private touchFile(filePath: string): void {
80
+ if (!this.current) return;
81
+
82
+ // Normalize path to relative
83
+ const relativePath = this.toRelativePath(filePath);
84
+ const existing = this.current.files.find(f => f.path === relativePath);
85
+
86
+ if (existing) {
87
+ existing.lastTouched = new Date();
88
+ existing.touchCount++;
89
+ } else {
90
+ this.current.files.push({
91
+ path: relativePath,
92
+ lastTouched: new Date(),
93
+ touchCount: 1,
94
+ recentLines: []
95
+ });
96
+
97
+ // Trim if over limit - keep most touched files
98
+ if (this.current.files.length > MAX_FILES) {
99
+ this.current.files = this.current.files
100
+ .sort((a, b) => b.touchCount - a.touchCount)
101
+ .slice(0, MAX_FILES);
102
+ }
103
+ }
104
+
105
+ this.current.lastActiveAt = new Date();
106
+ this.save();
107
+ }
108
+
109
+ // ========== CHANGE TRACKING ==========
110
+
111
+ private recordChange(filePath: string, diff: string, linesChanged: number[]): void {
112
+ if (!this.current) return;
113
+
114
+ const relativePath = this.toRelativePath(filePath);
115
+
116
+ this.current.changes.unshift({
117
+ file: relativePath,
118
+ timestamp: new Date(),
119
+ diff: diff.slice(0, 200), // Limit diff size
120
+ linesChanged
121
+ });
122
+
123
+ // Trim old changes
124
+ if (this.current.changes.length > MAX_CHANGES) {
125
+ this.current.changes = this.current.changes.slice(0, MAX_CHANGES);
126
+ }
127
+
128
+ // Update file's recent lines
129
+ const fileEntry = this.current.files.find(f => f.path === relativePath);
130
+ if (fileEntry && linesChanged.length > 0) {
131
+ fileEntry.recentLines = linesChanged.slice(0, 10);
132
+ }
133
+
134
+ this.save();
135
+ }
136
+
137
+ // ========== QUERY TRACKING ==========
138
+
139
+ onQuery(query: string, filesUsed: string[]): void {
140
+ this.ensureContext();
141
+ if (!this.current) return;
142
+
143
+ const relativeFiles = filesUsed.map(f => this.toRelativePath(f));
144
+
145
+ this.current.queries.unshift({
146
+ query,
147
+ timestamp: new Date(),
148
+ filesUsed: relativeFiles
149
+ });
150
+
151
+ // Trim old queries
152
+ if (this.current.queries.length > MAX_QUERIES) {
153
+ this.current.queries = this.current.queries.slice(0, MAX_QUERIES);
154
+ }
155
+
156
+ // Touch files that were used
157
+ relativeFiles.forEach(f => this.touchFile(f));
158
+
159
+ this.current.lastActiveAt = new Date();
160
+ this.save();
161
+ }
162
+
163
+ // ========== HOT CACHE ==========
164
+
165
+ private async preloadFile(filePath: string): Promise<void> {
166
+ const relativePath = this.toRelativePath(filePath);
167
+
168
+ if (this.fileContents.has(relativePath)) return;
169
+
170
+ try {
171
+ const absolutePath = this.toAbsolutePath(relativePath);
172
+ const content = readFileSync(absolutePath, 'utf-8');
173
+ this.fileContents.set(relativePath, content);
174
+
175
+ // Trim cache if too large
176
+ if (this.fileContents.size > HOT_CACHE_MAX_FILES && this.current) {
177
+ // Find least recently touched file and remove it
178
+ const filesInContext = new Set(this.current.files.map(f => f.path));
179
+ for (const [path] of this.fileContents) {
180
+ if (!filesInContext.has(path)) {
181
+ this.fileContents.delete(path);
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ } catch {
187
+ // File might not exist or be unreadable
188
+ }
189
+ }
190
+
191
+ getFileContent(filePath: string): string | null {
192
+ const relativePath = this.toRelativePath(filePath);
193
+ return this.fileContents.get(relativePath) || null;
194
+ }
195
+
196
+ // ========== CONTEXT RETRIEVAL ==========
197
+
198
+ getHotContext(): HotContext {
199
+ if (!this.current) {
200
+ return { files: [], changes: [], queries: [], summary: '' };
201
+ }
202
+
203
+ // Rank files by touchCount * recency
204
+ const now = Date.now();
205
+ const rankedFiles = this.current.files
206
+ .map(f => ({
207
+ ...f,
208
+ score: f.touchCount * (1 / (now - f.lastTouched.getTime() + 1))
209
+ }))
210
+ .sort((a, b) => b.score - a.score)
211
+ .slice(0, 10);
212
+
213
+ return {
214
+ files: rankedFiles.map(f => ({
215
+ path: f.path,
216
+ content: this.fileContents.get(f.path) || null,
217
+ touchCount: f.touchCount
218
+ })),
219
+ changes: this.current.changes.slice(0, 10),
220
+ queries: this.current.queries.slice(0, 5),
221
+ summary: this.generateSummary()
222
+ };
223
+ }
224
+
225
+ private generateSummary(): string {
226
+ if (!this.current) return '';
227
+
228
+ const topFiles = this.current.files
229
+ .sort((a, b) => b.touchCount - a.touchCount)
230
+ .slice(0, 5)
231
+ .map(f => basename(f.path))
232
+ .join(', ');
233
+
234
+ const recentChanges = this.current.changes.length;
235
+ const durationMs = Date.now() - this.current.startedAt.getTime();
236
+ const durationMin = Math.round(durationMs / 60000);
237
+
238
+ if (topFiles) {
239
+ return `Working on: ${topFiles} | ${recentChanges} changes | ${durationMin} min`;
240
+ }
241
+
242
+ return `Session active | ${durationMin} min`;
243
+ }
244
+
245
+ getCurrentContext(): ActiveFeatureContext | null {
246
+ return this.current;
247
+ }
248
+
249
+ getRecentContexts(): ActiveFeatureContext[] {
250
+ return this.recent;
251
+ }
252
+
253
+ getCurrentSummary(): { name: string; files: number; changes: number; duration: number } | null {
254
+ if (!this.current) return null;
255
+
256
+ return {
257
+ name: this.current.name,
258
+ files: this.current.files.length,
259
+ changes: this.current.changes.length,
260
+ duration: Math.round((Date.now() - this.current.startedAt.getTime()) / 60000)
261
+ };
262
+ }
263
+
264
+ // ========== CONTEXT MANAGEMENT ==========
265
+
266
+ private ensureContext(): void {
267
+ if (!this.current || this.current.status !== 'active') {
268
+ this.startNewContext();
269
+ }
270
+ }
271
+
272
+ startNewContext(name?: string): ActiveFeatureContext {
273
+ // Save current context to recent
274
+ if (this.current) {
275
+ this.current.status = 'paused';
276
+ this.recent.unshift(this.current);
277
+ this.recent = this.recent.slice(0, MAX_RECENT_CONTEXTS);
278
+ }
279
+
280
+ this.current = {
281
+ id: randomUUID(),
282
+ name: name || 'Untitled Feature',
283
+ files: [],
284
+ changes: [],
285
+ queries: [],
286
+ startedAt: new Date(),
287
+ lastActiveAt: new Date(),
288
+ status: 'active'
289
+ };
290
+
291
+ // Clear hot cache for new context
292
+ this.fileContents.clear();
293
+ this.save();
294
+
295
+ this.emit('context-started', this.current);
296
+ return this.current;
297
+ }
298
+
299
+ setContextName(name: string): boolean {
300
+ if (!this.current) return false;
301
+ this.current.name = name;
302
+ this.save();
303
+ return true;
304
+ }
305
+
306
+ switchToRecent(contextId: string): boolean {
307
+ const found = this.recent.find(c => c.id === contextId);
308
+ if (!found) return false;
309
+
310
+ // Save current to recent
311
+ if (this.current) {
312
+ this.current.status = 'paused';
313
+ this.recent.unshift(this.current);
314
+ }
315
+
316
+ // Remove found from recent and make it current
317
+ this.recent = this.recent.filter(c => c.id !== contextId);
318
+ this.current = found;
319
+ this.current.status = 'active';
320
+ this.current.lastActiveAt = new Date();
321
+
322
+ // Reload files into hot cache
323
+ this.reloadFiles();
324
+ this.save();
325
+
326
+ this.emit('context-switched', this.current);
327
+ return true;
328
+ }
329
+
330
+ private reloadFiles(): void {
331
+ this.fileContents.clear();
332
+ if (!this.current) return;
333
+
334
+ // Preload top 10 files
335
+ const topFiles = this.current.files
336
+ .sort((a, b) => b.touchCount - a.touchCount)
337
+ .slice(0, 10);
338
+
339
+ for (const file of topFiles) {
340
+ this.preloadFile(file.path);
341
+ }
342
+ }
343
+
344
+ completeContext(): boolean {
345
+ if (!this.current) return false;
346
+ this.current.status = 'completed';
347
+ this.recent.unshift(this.current);
348
+ this.recent = this.recent.slice(0, MAX_RECENT_CONTEXTS);
349
+ this.current = null;
350
+ this.fileContents.clear();
351
+ this.save();
352
+ return true;
353
+ }
354
+
355
+ // ========== AUTO MANAGEMENT ==========
356
+
357
+ private startInactivityTimer(): void {
358
+ if (this.inactivityTimer) {
359
+ clearInterval(this.inactivityTimer);
360
+ }
361
+
362
+ this.inactivityTimer = setInterval(() => {
363
+ if (!this.current) return;
364
+
365
+ const inactiveMs = Date.now() - this.current.lastActiveAt.getTime();
366
+ if (inactiveMs > TTL_MINUTES * 60 * 1000) {
367
+ this.current.status = 'paused';
368
+ this.emit('context-paused', this.current);
369
+ this.save();
370
+ }
371
+ }, 60000); // Check every minute
372
+ }
373
+
374
+ // ========== PERSISTENCE ==========
375
+
376
+ private save(): void {
377
+ try {
378
+ const dir = dirname(this.persistPath);
379
+ if (!existsSync(dir)) {
380
+ mkdirSync(dir, { recursive: true });
381
+ }
382
+
383
+ const data = {
384
+ current: this.current,
385
+ recent: this.recent
386
+ };
387
+
388
+ writeFileSync(this.persistPath, JSON.stringify(data, null, 2));
389
+ } catch (error) {
390
+ console.error('Error saving feature context:', error);
391
+ }
392
+ }
393
+
394
+ private load(): void {
395
+ try {
396
+ if (existsSync(this.persistPath)) {
397
+ const data = JSON.parse(readFileSync(this.persistPath, 'utf-8'));
398
+
399
+ // Parse dates in current context
400
+ if (data.current) {
401
+ this.current = this.parseContext(data.current);
402
+ }
403
+
404
+ // Parse dates in recent contexts
405
+ if (data.recent) {
406
+ this.recent = data.recent.map((c: ActiveFeatureContext) => this.parseContext(c));
407
+ }
408
+
409
+ // Reload files if we have a current context
410
+ if (this.current && this.current.status === 'active') {
411
+ this.reloadFiles();
412
+ }
413
+ }
414
+ } catch (error) {
415
+ console.error('Error loading feature context:', error);
416
+ this.current = null;
417
+ this.recent = [];
418
+ }
419
+ }
420
+
421
+ private parseContext(data: ActiveFeatureContext): ActiveFeatureContext {
422
+ return {
423
+ ...data,
424
+ startedAt: new Date(data.startedAt),
425
+ lastActiveAt: new Date(data.lastActiveAt),
426
+ files: data.files.map(f => ({
427
+ ...f,
428
+ lastTouched: new Date(f.lastTouched)
429
+ })),
430
+ changes: data.changes.map(c => ({
431
+ ...c,
432
+ timestamp: new Date(c.timestamp)
433
+ })),
434
+ queries: data.queries.map(q => ({
435
+ ...q,
436
+ timestamp: new Date(q.timestamp)
437
+ }))
438
+ };
439
+ }
440
+
441
+ // ========== PATH UTILITIES ==========
442
+
443
+ private toRelativePath(filePath: string): string {
444
+ if (filePath.startsWith(this.projectPath)) {
445
+ return filePath.slice(this.projectPath.length).replace(/^[\/\\]/, '');
446
+ }
447
+ return filePath;
448
+ }
449
+
450
+ private toAbsolutePath(relativePath: string): string {
451
+ if (relativePath.startsWith(this.projectPath)) {
452
+ return relativePath;
453
+ }
454
+ return join(this.projectPath, relativePath);
455
+ }
456
+
457
+ // ========== CONTEXT RESURRECTION ==========
458
+
459
+ /**
460
+ * Resurrect context from last session - restore mental state
461
+ * Returns what you were working on, where you left off, and possible blockers
462
+ */
463
+ resurrectContext(options: ContextResurrectionOptions = {}): ResurrectedContext {
464
+ const { featureName, maxFiles = 5 } = options;
465
+
466
+ // Find the context to resurrect
467
+ let contextToResurrect: ActiveFeatureContext | null = null;
468
+
469
+ if (featureName) {
470
+ // Find by name
471
+ contextToResurrect = this.recent.find(c =>
472
+ c.name.toLowerCase().includes(featureName.toLowerCase())
473
+ ) || null;
474
+
475
+ if (!contextToResurrect && this.current?.name.toLowerCase().includes(featureName.toLowerCase())) {
476
+ contextToResurrect = this.current;
477
+ }
478
+ } else {
479
+ // Use most recent context (current or first in recent)
480
+ contextToResurrect = this.current || this.recent[0] || null;
481
+ }
482
+
483
+ if (!contextToResurrect) {
484
+ return {
485
+ activeFiles: [],
486
+ lastQueries: [],
487
+ sessionDecisions: [],
488
+ lastEditedFile: null,
489
+ lastEditTime: null,
490
+ possibleBlocker: null,
491
+ suggestedActions: ['Start a new feature context with memory_record'],
492
+ summary: 'No previous context found to resurrect.',
493
+ timeSinceLastActive: 'N/A',
494
+ };
495
+ }
496
+
497
+ // Extract information from context
498
+ const activeFiles = contextToResurrect.files
499
+ .sort((a, b) => b.touchCount - a.touchCount)
500
+ .slice(0, maxFiles)
501
+ .map(f => f.path);
502
+
503
+ const lastQueries = contextToResurrect.queries
504
+ .slice(0, 3)
505
+ .map(q => q.query);
506
+
507
+ // Find last edited file (most recent change)
508
+ const lastChange = contextToResurrect.changes[0];
509
+ const lastEditedFile = lastChange?.file || null;
510
+ const lastEditTime = lastChange?.timestamp || null;
511
+
512
+ // Detect possible blocker
513
+ const possibleBlocker = this.detectBlocker(contextToResurrect);
514
+
515
+ // Generate suggested actions
516
+ const suggestedActions = this.suggestNextSteps(contextToResurrect);
517
+
518
+ // Calculate time since last active
519
+ const timeSinceLastActive = this.formatTimeSince(contextToResurrect.lastActiveAt);
520
+
521
+ // Generate summary
522
+ const summary = this.generateResurrectionSummary(contextToResurrect, possibleBlocker);
523
+
524
+ return {
525
+ activeFiles,
526
+ lastQueries,
527
+ sessionDecisions: [], // Would need to cross-reference with decisions storage
528
+ lastEditedFile,
529
+ lastEditTime,
530
+ possibleBlocker,
531
+ suggestedActions,
532
+ summary,
533
+ timeSinceLastActive,
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Detect what might have been blocking progress when session ended
539
+ */
540
+ private detectBlocker(context: ActiveFeatureContext): string | null {
541
+ if (context.queries.length === 0) {
542
+ return null;
543
+ }
544
+
545
+ // Check last few queries for error/problem patterns
546
+ const errorPatterns = [
547
+ /error/i,
548
+ /fix/i,
549
+ /bug/i,
550
+ /issue/i,
551
+ /problem/i,
552
+ /not working/i,
553
+ /doesn't work/i,
554
+ /how to/i,
555
+ /why/i,
556
+ /failed/i,
557
+ /broken/i,
558
+ ];
559
+
560
+ // Check last 3 queries
561
+ for (let i = 0; i < Math.min(3, context.queries.length); i++) {
562
+ const query = context.queries[i];
563
+ if (!query) continue;
564
+
565
+ for (const pattern of errorPatterns) {
566
+ if (pattern.test(query.query)) {
567
+ return query.query;
568
+ }
569
+ }
570
+ }
571
+
572
+ // Check if the same file was touched many times (sign of struggling)
573
+ const fileTouchCounts = new Map<string, number>();
574
+ for (const change of context.changes.slice(0, 10)) {
575
+ const count = fileTouchCounts.get(change.file) || 0;
576
+ fileTouchCounts.set(change.file, count + 1);
577
+ }
578
+
579
+ for (const [file, count] of fileTouchCounts.entries()) {
580
+ if (count >= 5) {
581
+ return `Multiple edits to ${basename(file)} - possible implementation challenge`;
582
+ }
583
+ }
584
+
585
+ return null;
586
+ }
587
+
588
+ /**
589
+ * Suggest next steps based on context state
590
+ */
591
+ private suggestNextSteps(context: ActiveFeatureContext): string[] {
592
+ const suggestions: string[] = [];
593
+
594
+ // If there was a potential blocker, suggest addressing it
595
+ const blocker = this.detectBlocker(context);
596
+ if (blocker) {
597
+ suggestions.push(`Resume investigating: "${blocker.slice(0, 50)}..."`);
598
+ }
599
+
600
+ // Suggest continuing with most-touched files
601
+ const topFiles = context.files
602
+ .sort((a, b) => b.touchCount - a.touchCount)
603
+ .slice(0, 2);
604
+
605
+ if (topFiles.length > 0 && topFiles[0]) {
606
+ suggestions.push(`Continue working on ${basename(topFiles[0].path)}`);
607
+ }
608
+
609
+ // If there were recent changes, suggest reviewing them
610
+ if (context.changes.length > 0) {
611
+ suggestions.push('Review recent changes before continuing');
612
+ }
613
+
614
+ // Generic suggestions if nothing specific
615
+ if (suggestions.length === 0) {
616
+ suggestions.push(`Continue "${context.name}" feature work`);
617
+ }
618
+
619
+ return suggestions;
620
+ }
621
+
622
+ /**
623
+ * Format time since a date in human-readable form
624
+ */
625
+ private formatTimeSince(date: Date): string {
626
+ const now = new Date();
627
+ const diffMs = now.getTime() - date.getTime();
628
+ const diffMins = Math.floor(diffMs / (1000 * 60));
629
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
630
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
631
+
632
+ if (diffMins < 60) {
633
+ return `${diffMins} minutes ago`;
634
+ } else if (diffHours < 24) {
635
+ return `${diffHours} hours ago`;
636
+ } else if (diffDays === 1) {
637
+ return 'yesterday';
638
+ } else if (diffDays < 7) {
639
+ return `${diffDays} days ago`;
640
+ } else if (diffDays < 30) {
641
+ const weeks = Math.floor(diffDays / 7);
642
+ return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
643
+ } else {
644
+ const months = Math.floor(diffDays / 30);
645
+ return `${months} month${months > 1 ? 's' : ''} ago`;
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Generate a summary of the resurrection context for AI
651
+ */
652
+ private generateResurrectionSummary(context: ActiveFeatureContext, blocker: string | null): string {
653
+ const parts: string[] = [];
654
+
655
+ // Feature name and duration
656
+ const durationMs = context.lastActiveAt.getTime() - context.startedAt.getTime();
657
+ const durationMins = Math.round(durationMs / 60000);
658
+ parts.push(`Feature: "${context.name}" (${durationMins} min session)`);
659
+
660
+ // Status
661
+ parts.push(`Status: ${context.status}`);
662
+
663
+ // Files worked on
664
+ if (context.files.length > 0) {
665
+ const topFiles = context.files
666
+ .sort((a, b) => b.touchCount - a.touchCount)
667
+ .slice(0, 3)
668
+ .map(f => basename(f.path));
669
+ parts.push(`Main files: ${topFiles.join(', ')}`);
670
+ }
671
+
672
+ // Changes made
673
+ if (context.changes.length > 0) {
674
+ parts.push(`Changes: ${context.changes.length} edits`);
675
+ }
676
+
677
+ // Blocker
678
+ if (blocker) {
679
+ parts.push(`Possible blocker: "${blocker.slice(0, 50)}..."`);
680
+ }
681
+
682
+ // Last activity
683
+ parts.push(`Last active: ${this.formatTimeSince(context.lastActiveAt)}`);
684
+
685
+ return parts.join(' | ');
686
+ }
687
+
688
+ /**
689
+ * Get all contexts that can be resurrected
690
+ */
691
+ getResurrectableContexts(): Array<{ id: string; name: string; lastActive: Date; summary: string }> {
692
+ const contexts: Array<{ id: string; name: string; lastActive: Date; summary: string }> = [];
693
+
694
+ // Include current if paused
695
+ if (this.current && this.current.status === 'paused') {
696
+ contexts.push({
697
+ id: this.current.id,
698
+ name: this.current.name,
699
+ lastActive: this.current.lastActiveAt,
700
+ summary: this.generateResurrectionSummary(this.current, null),
701
+ });
702
+ }
703
+
704
+ // Include recent contexts
705
+ for (const ctx of this.recent) {
706
+ contexts.push({
707
+ id: ctx.id,
708
+ name: ctx.name,
709
+ lastActive: ctx.lastActiveAt,
710
+ summary: this.generateResurrectionSummary(ctx, null),
711
+ });
712
+ }
713
+
714
+ return contexts.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime());
715
+ }
716
+
717
+ // ========== CLEANUP ==========
718
+
719
+ shutdown(): void {
720
+ if (this.inactivityTimer) {
721
+ clearInterval(this.inactivityTimer);
722
+ this.inactivityTimer = null;
723
+ }
724
+ this.save();
725
+ }
726
+ }