55 */
66
77import { execSync } from 'child_process' ;
8- import { existsSync , readFileSync , readdirSync , statSync } from 'fs' ;
8+ import {
9+ existsSync ,
10+ readFileSync ,
11+ readdirSync ,
12+ statSync ,
13+ writeFileSync ,
14+ mkdirSync ,
15+ } from 'fs' ;
916import { join , basename } from 'path' ;
10- import { homedir } from 'os' ;
17+ import { homedir , tmpdir } from 'os' ;
18+ import { globSync } from 'glob' ;
1119
1220// Load session decisions if available
1321interface SessionDecision {
@@ -19,6 +27,20 @@ interface SessionDecision {
1927 category ?: string ;
2028}
2129
30+ // Review feedback persistence
31+ interface StoredReviewFeedback {
32+ timestamp : string ;
33+ source : string ;
34+ keyPoints : string [ ] ;
35+ actionItems : string [ ] ;
36+ sourceFile ?: string ;
37+ }
38+
39+ interface ReviewFeedbackStore {
40+ feedbacks : StoredReviewFeedback [ ] ;
41+ lastUpdated : string ;
42+ }
43+
2244function loadSessionDecisions ( projectRoot : string ) : SessionDecision [ ] {
2345 const storePath = join ( projectRoot , '.stackmemory' , 'session-decisions.json' ) ;
2446 if ( existsSync ( storePath ) ) {
@@ -32,6 +54,124 @@ function loadSessionDecisions(projectRoot: string): SessionDecision[] {
3254 return [ ] ;
3355}
3456
57+ function loadReviewFeedback ( projectRoot : string ) : StoredReviewFeedback [ ] {
58+ const storePath = join ( projectRoot , '.stackmemory' , 'review-feedback.json' ) ;
59+ if ( existsSync ( storePath ) ) {
60+ try {
61+ const store : ReviewFeedbackStore = JSON . parse (
62+ readFileSync ( storePath , 'utf-8' )
63+ ) ;
64+ // Return feedbacks from last 24 hours
65+ const cutoff = Date . now ( ) - 24 * 60 * 60 * 1000 ;
66+ return store . feedbacks . filter (
67+ ( f ) => new Date ( f . timestamp ) . getTime ( ) > cutoff
68+ ) ;
69+ } catch {
70+ return [ ] ;
71+ }
72+ }
73+ return [ ] ;
74+ }
75+
76+ function saveReviewFeedback (
77+ projectRoot : string ,
78+ feedbacks : StoredReviewFeedback [ ]
79+ ) : void {
80+ const dir = join ( projectRoot , '.stackmemory' ) ;
81+ if ( ! existsSync ( dir ) ) {
82+ mkdirSync ( dir , { recursive : true } ) ;
83+ }
84+
85+ const storePath = join ( dir , 'review-feedback.json' ) ;
86+
87+ // Load existing and merge
88+ let existing : StoredReviewFeedback [ ] = [ ] ;
89+ if ( existsSync ( storePath ) ) {
90+ try {
91+ const store : ReviewFeedbackStore = JSON . parse (
92+ readFileSync ( storePath , 'utf-8' )
93+ ) ;
94+ existing = store . feedbacks || [ ] ;
95+ } catch {
96+ // Ignore parse errors
97+ }
98+ }
99+
100+ // Deduplicate by source + first key point
101+ const seen = new Set < string > ( ) ;
102+ const merged : StoredReviewFeedback [ ] = [ ] ;
103+
104+ for ( const f of [ ...feedbacks , ...existing ] ) {
105+ const key = `${ f . source } :${ f . keyPoints [ 0 ] || '' } ` ;
106+ if ( ! seen . has ( key ) ) {
107+ seen . add ( key ) ;
108+ merged . push ( f ) ;
109+ }
110+ }
111+
112+ // Keep only last 20 feedbacks
113+ const store : ReviewFeedbackStore = {
114+ feedbacks : merged . slice ( 0 , 20 ) ,
115+ lastUpdated : new Date ( ) . toISOString ( ) ,
116+ } ;
117+
118+ writeFileSync ( storePath , JSON . stringify ( store , null , 2 ) ) ;
119+ }
120+
121+ /**
122+ * Find Claude agent output directories dynamically
123+ */
124+ function findAgentOutputDirs ( projectRoot : string ) : string [ ] {
125+ const dirs : string [ ] = [ ] ;
126+
127+ // Try multiple locations where agent outputs might be stored
128+ const tmpBase = process . env [ 'TMPDIR' ] || tmpdir ( ) || '/tmp' ;
129+
130+ // Pattern 1: /tmp/claude/-path-to-project/tasks
131+ const projectPathEncoded = projectRoot . replace ( / \/ / g, '-' ) . replace ( / ^ - / , '' ) ;
132+ const pattern1 = join ( tmpBase , 'claude' , `*${ projectPathEncoded } *` , 'tasks' ) ;
133+ try {
134+ const matches = globSync ( pattern1 ) ;
135+ dirs . push ( ...matches ) ;
136+ } catch {
137+ // Glob failed
138+ }
139+
140+ // Pattern 2: /private/tmp/claude/... (macOS specific)
141+ if ( tmpBase !== '/private/tmp' ) {
142+ const pattern2 = join (
143+ '/private/tmp' ,
144+ 'claude' ,
145+ `*${ projectPathEncoded } *` ,
146+ 'tasks'
147+ ) ;
148+ try {
149+ const matches = globSync ( pattern2 ) ;
150+ dirs . push ( ...matches ) ;
151+ } catch {
152+ // Glob failed
153+ }
154+ }
155+
156+ // Pattern 3: ~/.claude/projects/*/tasks (if exists)
157+ const homeClaudeDir = join ( homedir ( ) , '.claude' , 'projects' ) ;
158+ if ( existsSync ( homeClaudeDir ) ) {
159+ try {
160+ const projectDirs = readdirSync ( homeClaudeDir ) ;
161+ for ( const d of projectDirs ) {
162+ const tasksDir = join ( homeClaudeDir , d , 'tasks' ) ;
163+ if ( existsSync ( tasksDir ) ) {
164+ dirs . push ( tasksDir ) ;
165+ }
166+ }
167+ } catch {
168+ // Failed to read
169+ }
170+ }
171+
172+ return [ ...new Set ( dirs ) ] ; // Deduplicate
173+ }
174+
35175export interface EnhancedHandoff {
36176 // Metadata
37177 timestamp : string ;
@@ -387,16 +527,20 @@ export class EnhancedHandoffGenerator {
387527 }
388528
389529 /**
390- * Extract review feedback from agent output files
530+ * Extract review feedback from agent output files and persisted storage
391531 */
392532 private async extractReviewFeedback ( ) : Promise <
393533 EnhancedHandoff [ 'reviewFeedback' ]
394534 > {
395535 const feedback : EnhancedHandoff [ 'reviewFeedback' ] = [ ] ;
536+ const newFeedbacks : StoredReviewFeedback [ ] = [ ] ;
537+
538+ // Find agent output directories dynamically
539+ const outputDirs = findAgentOutputDirs ( this . projectRoot ) ;
540+
541+ for ( const tmpDir of outputDirs ) {
542+ if ( ! existsSync ( tmpDir ) ) continue ;
396543
397- // Look for agent output files in temp directory
398- const tmpDir = '/private/tmp/claude/-Users-jwu-Dev-stackmemory/tasks' ;
399- if ( existsSync ( tmpDir ) ) {
400544 try {
401545 const files = readdirSync ( tmpDir ) . filter ( ( f ) => f . endsWith ( '.output' ) ) ;
402546 const recentFiles = files
@@ -414,10 +558,36 @@ export class EnhancedHandoffGenerator {
414558 const extracted = this . extractKeyPointsFromReview ( content ) ;
415559 if ( extracted . keyPoints . length > 0 ) {
416560 feedback . push ( extracted ) ;
561+
562+ // Also store for persistence
563+ newFeedbacks . push ( {
564+ timestamp : new Date ( ) . toISOString ( ) ,
565+ source : extracted . source ,
566+ keyPoints : extracted . keyPoints ,
567+ actionItems : extracted . actionItems ,
568+ sourceFile : file . name ,
569+ } ) ;
417570 }
418571 }
419572 } catch {
420- // Failed to read agent outputs
573+ // Failed to read agent outputs from this directory
574+ }
575+ }
576+
577+ // Save new feedback to persistent storage
578+ if ( newFeedbacks . length > 0 ) {
579+ saveReviewFeedback ( this . projectRoot , newFeedbacks ) ;
580+ }
581+
582+ // Load persisted feedback if no new feedback found
583+ if ( feedback . length === 0 ) {
584+ const stored = loadReviewFeedback ( this . projectRoot ) ;
585+ for ( const s of stored . slice ( 0 , 3 ) ) {
586+ feedback . push ( {
587+ source : s . source ,
588+ keyPoints : s . keyPoints ,
589+ actionItems : s . actionItems ,
590+ } ) ;
421591 }
422592 }
423593
0 commit comments