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
- **With grov:** Same task takes ~1-2 minutes, <2% tokens, 0 explore agents. Claude reads files directly because it already has context.
38
+ ![grov status](docs/grov-status.jpeg)
39
39
 
40
- <sub>*Based on controlled testing: Auth file modification task without grov launched 3+ subagents and read ~10 files for exploration. With grov (after initial memory capture), an adjacent task went directly to reading relevant files with full context.</sub>
40
+ ### What Gets Captured
41
+
42
+ Real reasoning, not just file lists:
43
+
44
+ ![captured reasoning](docs/reasoning-output.jpeg)
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": "compressed summary of assistant's actions and reasoning (max 800 chars)"`
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: Summarize what the assistant did and WHY in a concise way (max 800 chars)'
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 = `Analyze Claude's work session and extract structured reasoning and decisions.
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 WORK (reasoning from each step):
668
+ CLAUDE'S RESPONSE:
669
669
  ${combinedReasoning}
670
670
 
671
671
  ═══════════════════════════════════════════════════════════════
672
- EXTRACT TWO THINGS:
672
+ EXTRACT ACTIONABLE CONCLUSIONS - NOT PROCESS
673
673
  ═══════════════════════════════════════════════════════════════
674
674
 
675
- 1. REASONING TRACE (what was done and WHY):
676
- - Each entry: "[ACTION] [target] because/to [reason]"
677
- - Include specific file names, function names when mentioned
678
- - Focus on WHY decisions were made, not just what
679
- - Max 10 entries, most important first
680
-
681
- 2. DECISIONS (choices made between alternatives):
682
- - Only include actual choices/tradeoffs
683
- - Each must have: what was chosen, why it was chosen
684
- - Examples: "Chose X over Y because Z"
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
- "Created auth/token-store.ts to separate storage logic from validation",
691
- "Used Map instead of Object for O(1) lookup performance",
692
- "Added expiry check in validateToken to prevent stale token usage"
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": "Used SHA256 for token hashing", "reason": "Fast, secure enough for this use case, no external deps"},
696
- {"choice": "Split into separate files", "reason": "Better testability and single responsibility"}
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
- - Be specific, not vague (bad: "Fixed bug", good: "Fixed null check in validateToken")`;
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('[GROV CONTEXT - Relevant past reasoning]');
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(`You may already have context for: ${files.join(', ')}`);
73
+ lines.push(`Files with existing context: ${files.join(', ')}`);
71
74
  }
72
- lines.push('[END GROV CONTEXT]');
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
- const filePattern = /(?:^|\s|["'`])([\/\w.-]+\.[a-zA-Z]{1,10})(?:["'`]|\s|$|:|\))/g;
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(msg.content)) !== null) {
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 filesTouched = [...new Set(steps.flatMap(s => s.files))];
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')
@@ -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
- // Inject context from team memory
302
- const mentionedFiles = extractFilesFromMessages(modified.messages || []);
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 LATEST user message (not first!)
824
- * Filters out system-reminder tags to get the actual user prompt
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 lastUser = userMessages[userMessages.length - 1];
830
- if (!lastUser)
831
- return undefined;
832
- let rawContent = '';
833
- // Handle string content
834
- if (typeof lastUser.content === 'string') {
835
- rawContent = lastUser.content;
836
- }
837
- // Handle array content (new API format)
838
- if (Array.isArray(lastUser.content)) {
839
- const textBlocks = lastUser.content
840
- .filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
841
- .map(block => block.text);
842
- rawContent = textBlocks.join('\n');
843
- }
844
- // Remove <system-reminder>...</system-reminder> tags
845
- const cleanContent = rawContent
846
- .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
847
- .trim();
848
- // If nothing left after removing reminders, return undefined
849
- if (!cleanContent || cleanContent.length < 5) {
850
- return undefined;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grov",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Collective AI memory for Claude Code - captures reasoning from sessions and injects context into future sessions",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",