grov 0.2.2 → 0.2.3
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/README.md
CHANGED
|
@@ -35,9 +35,15 @@ Every time you start a new Claude Code session:
|
|
|
35
35
|
|
|
36
36
|
Grov captures what Claude learns and injects it back on the next session.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+

|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
### What Gets Captured
|
|
41
|
+
|
|
42
|
+
Real reasoning, not just file lists:
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
*Architectural decisions, patterns, and rationale - automatically extracted.*
|
|
41
47
|
|
|
42
48
|
## Quick Start
|
|
43
49
|
|
|
@@ -544,10 +544,10 @@ export async function analyzeTaskContext(currentSession, latestUserMessage, rece
|
|
|
544
544
|
// Check if we need to compress reasoning
|
|
545
545
|
const needsCompression = assistantResponse.length > 1000;
|
|
546
546
|
const compressionInstruction = needsCompression
|
|
547
|
-
? `\n "step_reasoning": "
|
|
547
|
+
? `\n "step_reasoning": "Extract CONCLUSIONS and SPECIFIC RECOMMENDATIONS only. Include: exact file paths (e.g., src/lib/utils.ts), function/component names, architectural patterns discovered, and WHY decisions were made. DO NOT write process descriptions like 'explored' or 'analyzed'. Max 800 chars."`
|
|
548
548
|
: '';
|
|
549
549
|
const compressionRule = needsCompression
|
|
550
|
-
? '\n- step_reasoning:
|
|
550
|
+
? '\n- step_reasoning: Extract CONCLUSIONS (specific files, patterns, decisions) NOT process descriptions. Example GOOD: "Utilities belong in src/lib/utils.ts alongside cn(), formatDate()". Example BAD: "Explored codebase structure".'
|
|
551
551
|
: '';
|
|
552
552
|
// Extract topic keywords from goal for comparison
|
|
553
553
|
const currentGoalKeywords = currentSession?.original_goal
|
|
@@ -660,40 +660,51 @@ export async function extractReasoningAndDecisions(stepsReasoning, originalGoal)
|
|
|
660
660
|
if (combinedReasoning.length < 50) {
|
|
661
661
|
return { reasoning_trace: [], decisions: [] };
|
|
662
662
|
}
|
|
663
|
-
const prompt = `
|
|
663
|
+
const prompt = `Extract CONCLUSIONS and KNOWLEDGE from Claude's work - NOT process descriptions.
|
|
664
664
|
|
|
665
665
|
ORIGINAL GOAL:
|
|
666
666
|
${originalGoal || 'Not specified'}
|
|
667
667
|
|
|
668
|
-
CLAUDE'S
|
|
668
|
+
CLAUDE'S RESPONSE:
|
|
669
669
|
${combinedReasoning}
|
|
670
670
|
|
|
671
671
|
═══════════════════════════════════════════════════════════════
|
|
672
|
-
EXTRACT
|
|
672
|
+
EXTRACT ACTIONABLE CONCLUSIONS - NOT PROCESS
|
|
673
673
|
═══════════════════════════════════════════════════════════════
|
|
674
674
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
675
|
+
GOOD examples (specific, reusable knowledge):
|
|
676
|
+
- "Utility functions belong in frontend/lib/utils.ts - existing utils: cn(), formatDate(), debounce()"
|
|
677
|
+
- "Auth tokens stored in localStorage with 15min expiry for long form sessions"
|
|
678
|
+
- "API routes follow REST pattern in /api/v1/ with Zod validation"
|
|
679
|
+
- "Database migrations go in prisma/migrations/ using prisma migrate"
|
|
680
|
+
|
|
681
|
+
BAD examples (process descriptions - DO NOT EXTRACT THESE):
|
|
682
|
+
- "Explored the codebase structure"
|
|
683
|
+
- "Analyzed several approaches"
|
|
684
|
+
- "Searched for utility directories"
|
|
685
|
+
- "Looked at the file organization"
|
|
686
|
+
|
|
687
|
+
1. REASONING TRACE (conclusions and recommendations):
|
|
688
|
+
- WHAT was discovered or decided (specific file paths, patterns)
|
|
689
|
+
- WHY this is the right approach
|
|
690
|
+
- WHERE this applies in the codebase
|
|
691
|
+
- Max 10 entries, prioritize specific file/function recommendations
|
|
692
|
+
|
|
693
|
+
2. DECISIONS (architectural choices):
|
|
694
|
+
- Only significant choices that affect future work
|
|
695
|
+
- What was chosen and why
|
|
685
696
|
- Max 5 decisions
|
|
686
697
|
|
|
687
698
|
Return JSON:
|
|
688
699
|
{
|
|
689
700
|
"reasoning_trace": [
|
|
690
|
-
"
|
|
691
|
-
"
|
|
692
|
-
"
|
|
701
|
+
"Utility functions belong in frontend/lib/utils.ts alongside cn(), formatDate(), debounce(), generateId()",
|
|
702
|
+
"Backend utilities go in backend/app/utils/ with domain-specific files like validation.py",
|
|
703
|
+
"The @/lib/utils import alias is configured for frontend utility access"
|
|
693
704
|
],
|
|
694
705
|
"decisions": [
|
|
695
|
-
{"choice": "
|
|
696
|
-
{"choice": "
|
|
706
|
+
{"choice": "Add to existing utils.ts rather than new file", "reason": "Maintains established pattern, easier discoverability"},
|
|
707
|
+
{"choice": "Use frontend/lib/ over src/utils/", "reason": "Follows Next.js conventions used throughout project"}
|
|
697
708
|
]
|
|
698
709
|
}
|
|
699
710
|
|
|
@@ -701,7 +712,8 @@ RESPONSE RULES:
|
|
|
701
712
|
- English only
|
|
702
713
|
- No emojis
|
|
703
714
|
- Valid JSON only
|
|
704
|
-
-
|
|
715
|
+
- Extract WHAT and WHERE, not just WHAT was done
|
|
716
|
+
- If no specific conclusions found, return empty arrays`;
|
|
705
717
|
debugLLM('extractReasoningAndDecisions', `Analyzing ${stepsReasoning.length} steps, ${combinedReasoning.length} chars`);
|
|
706
718
|
try {
|
|
707
719
|
const response = await client.messages.create({
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function buildTeamMemoryContext(projectPath: string, mentionedFiles: string[]): string | null;
|
|
6
6
|
/**
|
|
7
|
-
* Extract file paths from messages
|
|
7
|
+
* Extract file paths from messages (user messages only, clean text)
|
|
8
8
|
*/
|
|
9
9
|
export declare function extractFilesFromMessages(messages: Array<{
|
|
10
10
|
role: string;
|
|
@@ -32,7 +32,10 @@ export function buildTeamMemoryContext(projectPath, mentionedFiles) {
|
|
|
32
32
|
*/
|
|
33
33
|
function formatTeamMemoryContext(tasks, fileReasonings, files) {
|
|
34
34
|
const lines = [];
|
|
35
|
-
lines.push('
|
|
35
|
+
lines.push('=== VERIFIED TEAM KNOWLEDGE (from previous sessions) ===');
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push('IMPORTANT: This context has been verified. USE IT to answer directly.');
|
|
38
|
+
lines.push('DO NOT launch Explore agents or re-investigate files mentioned below.');
|
|
36
39
|
lines.push('');
|
|
37
40
|
// File-level context
|
|
38
41
|
if (fileReasonings.length > 0) {
|
|
@@ -67,21 +70,44 @@ function formatTeamMemoryContext(tasks, fileReasonings, files) {
|
|
|
67
70
|
lines.push('');
|
|
68
71
|
}
|
|
69
72
|
if (files.length > 0) {
|
|
70
|
-
lines.push(`
|
|
73
|
+
lines.push(`Files with existing context: ${files.join(', ')}`);
|
|
71
74
|
}
|
|
72
|
-
lines.push('
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push('Answer the user\'s question using the knowledge above. Skip exploration.');
|
|
77
|
+
lines.push('=== END VERIFIED TEAM KNOWLEDGE ===');
|
|
73
78
|
return lines.join('\n');
|
|
74
79
|
}
|
|
75
80
|
/**
|
|
76
|
-
* Extract file paths from messages
|
|
81
|
+
* Extract file paths from messages (user messages only, clean text)
|
|
77
82
|
*/
|
|
78
83
|
export function extractFilesFromMessages(messages) {
|
|
79
84
|
const files = [];
|
|
80
|
-
|
|
85
|
+
// Pattern matches filenames with extensions, allowing common punctuation after
|
|
86
|
+
const filePattern = /(?:^|\s|["'`])([\/\w.-]+\.[a-zA-Z]{1,10})(?:["'`]|\s|$|[:)\]?!,;])/g;
|
|
81
87
|
for (const msg of messages) {
|
|
88
|
+
// Only scan user messages for file mentions
|
|
89
|
+
if (msg.role !== 'user')
|
|
90
|
+
continue;
|
|
91
|
+
let textContent = '';
|
|
92
|
+
// Handle string content
|
|
82
93
|
if (typeof msg.content === 'string') {
|
|
94
|
+
textContent = msg.content;
|
|
95
|
+
}
|
|
96
|
+
// Handle array content (Claude Code API format)
|
|
97
|
+
if (Array.isArray(msg.content)) {
|
|
98
|
+
for (const block of msg.content) {
|
|
99
|
+
if (block && typeof block === 'object' && 'type' in block && block.type === 'text' && 'text' in block && typeof block.text === 'string') {
|
|
100
|
+
textContent += block.text + '\n';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Strip system-reminder tags to get clean user content
|
|
105
|
+
const cleanContent = textContent
|
|
106
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
107
|
+
.trim();
|
|
108
|
+
if (cleanContent) {
|
|
83
109
|
let match;
|
|
84
|
-
while ((match = filePattern.exec(
|
|
110
|
+
while ((match = filePattern.exec(cleanContent)) !== null) {
|
|
85
111
|
const path = match[1];
|
|
86
112
|
// Filter out common false positives
|
|
87
113
|
if (!path.includes('http') && !path.startsWith('.') && path.length > 3) {
|
|
@@ -24,8 +24,18 @@ export async function saveToTeamMemory(sessionId, triggerReason) {
|
|
|
24
24
|
* Build task data from session state and steps
|
|
25
25
|
*/
|
|
26
26
|
async function buildTaskFromSession(sessionState, steps, triggerReason) {
|
|
27
|
-
// Aggregate files from steps
|
|
28
|
-
const
|
|
27
|
+
// Aggregate files from steps (tool_use actions)
|
|
28
|
+
const stepFiles = steps.flatMap(s => s.files);
|
|
29
|
+
// Also extract file paths mentioned in reasoning text (Claude's text responses)
|
|
30
|
+
const reasoningFiles = steps
|
|
31
|
+
.filter(s => s.reasoning)
|
|
32
|
+
.flatMap(s => {
|
|
33
|
+
// Match common code file extensions
|
|
34
|
+
const matches = s.reasoning?.match(/[\w\/.-]+\.(ts|js|tsx|jsx|py|go|rs|java|css|html|md|json|yaml|yml)/g) || [];
|
|
35
|
+
// Filter out obvious non-paths (urls, version numbers)
|
|
36
|
+
return matches.filter(m => !m.includes('://') && !m.match(/^\d+\.\d+/));
|
|
37
|
+
});
|
|
38
|
+
const filesTouched = [...new Set([...stepFiles, ...reasoningFiles])];
|
|
29
39
|
// Build basic reasoning trace from steps (fallback)
|
|
30
40
|
const basicReasoningTrace = steps
|
|
31
41
|
.filter(s => s.is_key_decision || s.action_type === 'edit' || s.action_type === 'write')
|
package/dist/proxy/server.js
CHANGED
|
@@ -51,9 +51,7 @@ const activeSessions = new Map();
|
|
|
51
51
|
*/
|
|
52
52
|
export function createServer() {
|
|
53
53
|
const fastify = Fastify({
|
|
54
|
-
logger:
|
|
55
|
-
level: 'error', // Only errors in console, details in file
|
|
56
|
-
},
|
|
54
|
+
logger: false, // Disabled - all debug goes to ~/.grov/debug.log
|
|
57
55
|
bodyLimit: config.BODY_LIMIT,
|
|
58
56
|
});
|
|
59
57
|
// Health check endpoint
|
|
@@ -227,9 +225,16 @@ async function getOrCreateSession(request, logger) {
|
|
|
227
225
|
*/
|
|
228
226
|
async function preProcessRequest(body, sessionInfo, logger) {
|
|
229
227
|
const modified = { ...body };
|
|
228
|
+
// FIRST: Always inject team memory context (doesn't require sessionState)
|
|
229
|
+
const mentionedFiles = extractFilesFromMessages(modified.messages || []);
|
|
230
|
+
const teamContext = buildTeamMemoryContext(sessionInfo.projectPath, mentionedFiles);
|
|
231
|
+
if (teamContext) {
|
|
232
|
+
appendToSystemPrompt(modified, '\n\n' + teamContext);
|
|
233
|
+
}
|
|
234
|
+
// THEN: Session-specific operations
|
|
230
235
|
const sessionState = getSessionState(sessionInfo.sessionId);
|
|
231
236
|
if (!sessionState) {
|
|
232
|
-
return modified;
|
|
237
|
+
return modified; // Injection already happened above!
|
|
233
238
|
}
|
|
234
239
|
// Extract latest user message for drift checking
|
|
235
240
|
const latestUserMessage = extractGoalFromMessages(body.messages) || '';
|
|
@@ -298,18 +303,8 @@ Please continue from where you left off.`;
|
|
|
298
303
|
}
|
|
299
304
|
}
|
|
300
305
|
}
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
const teamContext = buildTeamMemoryContext(sessionInfo.projectPath, mentionedFiles);
|
|
304
|
-
if (teamContext) {
|
|
305
|
-
appendToSystemPrompt(modified, '\n\n' + teamContext);
|
|
306
|
-
logger.info({
|
|
307
|
-
msg: 'Injected team memory context',
|
|
308
|
-
filesMatched: mentionedFiles.length,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
// Log final system prompt size
|
|
312
|
-
const finalSystemSize = getSystemPromptText(modified).length;
|
|
306
|
+
// Note: Team memory context injection is now at the TOP of preProcessRequest()
|
|
307
|
+
// so it runs even when sessionState is null (new sessions)
|
|
313
308
|
return modified;
|
|
314
309
|
}
|
|
315
310
|
/**
|
|
@@ -820,36 +815,34 @@ function extractProjectPath(body) {
|
|
|
820
815
|
return null;
|
|
821
816
|
}
|
|
822
817
|
/**
|
|
823
|
-
* Extract goal from
|
|
824
|
-
*
|
|
818
|
+
* Extract goal from FIRST user message with text content
|
|
819
|
+
* Skips tool_result blocks, filters out system-reminder tags
|
|
825
820
|
*/
|
|
826
821
|
function extractGoalFromMessages(messages) {
|
|
827
|
-
// Find the LAST user message (most recent prompt)
|
|
828
822
|
const userMessages = messages?.filter(m => m.role === 'user') || [];
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
.
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return cleanContent.substring(0, 500);
|
|
823
|
+
for (const userMsg of userMessages) {
|
|
824
|
+
let rawContent = '';
|
|
825
|
+
// Handle string content
|
|
826
|
+
if (typeof userMsg.content === 'string') {
|
|
827
|
+
rawContent = userMsg.content;
|
|
828
|
+
}
|
|
829
|
+
// Handle array content - look for text blocks (skip tool_result)
|
|
830
|
+
if (Array.isArray(userMsg.content)) {
|
|
831
|
+
const textBlocks = userMsg.content
|
|
832
|
+
.filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
|
|
833
|
+
.map(block => block.text);
|
|
834
|
+
rawContent = textBlocks.join('\n');
|
|
835
|
+
}
|
|
836
|
+
// Remove <system-reminder>...</system-reminder> tags
|
|
837
|
+
const cleanContent = rawContent
|
|
838
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
839
|
+
.trim();
|
|
840
|
+
// If we found valid text content, return it
|
|
841
|
+
if (cleanContent && cleanContent.length >= 5) {
|
|
842
|
+
return cleanContent.substring(0, 500);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return undefined;
|
|
853
846
|
}
|
|
854
847
|
/**
|
|
855
848
|
* Filter response headers for forwarding to client
|
package/package.json
CHANGED