Skip to content

Commit d7ffccb

Browse files
fix: Dynamic path resolution and review feedback persistence
- Replace hardcoded /private/tmp path with dynamic resolution - Use glob to find agent output directories across platforms - Add review feedback persistence to .stackmemory/review-feedback.json - Load persisted feedback when no new feedback available - Keep last 20 feedbacks, auto-expire after 24 hours
1 parent ed7445d commit d7ffccb

1 file changed

Lines changed: 177 additions & 7 deletions

File tree

src/core/session/enhanced-handoff.ts

Lines changed: 177 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
55
*/
66

77
import { 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';
916
import { 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
1321
interface 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+
2244
function 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+
35175
export 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

Comments
 (0)