grov 0.1.1 → 0.2.2

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 (39) hide show
  1. package/README.md +66 -87
  2. package/dist/cli.js +23 -37
  3. package/dist/commands/capture.js +1 -1
  4. package/dist/commands/disable.d.ts +1 -0
  5. package/dist/commands/disable.js +14 -0
  6. package/dist/commands/drift-test.js +56 -68
  7. package/dist/commands/init.js +29 -17
  8. package/dist/commands/proxy-status.d.ts +1 -0
  9. package/dist/commands/proxy-status.js +32 -0
  10. package/dist/commands/unregister.js +7 -1
  11. package/dist/lib/correction-builder-proxy.d.ts +16 -0
  12. package/dist/lib/correction-builder-proxy.js +125 -0
  13. package/dist/lib/correction-builder.js +1 -1
  14. package/dist/lib/drift-checker-proxy.d.ts +63 -0
  15. package/dist/lib/drift-checker-proxy.js +373 -0
  16. package/dist/lib/drift-checker.js +1 -1
  17. package/dist/lib/hooks.d.ts +11 -0
  18. package/dist/lib/hooks.js +33 -0
  19. package/dist/lib/llm-extractor.d.ts +60 -11
  20. package/dist/lib/llm-extractor.js +419 -98
  21. package/dist/lib/settings.d.ts +19 -0
  22. package/dist/lib/settings.js +63 -0
  23. package/dist/lib/store.d.ts +201 -43
  24. package/dist/lib/store.js +653 -90
  25. package/dist/proxy/action-parser.d.ts +58 -0
  26. package/dist/proxy/action-parser.js +196 -0
  27. package/dist/proxy/config.d.ts +26 -0
  28. package/dist/proxy/config.js +67 -0
  29. package/dist/proxy/forwarder.d.ts +24 -0
  30. package/dist/proxy/forwarder.js +119 -0
  31. package/dist/proxy/index.d.ts +1 -0
  32. package/dist/proxy/index.js +30 -0
  33. package/dist/proxy/request-processor.d.ts +12 -0
  34. package/dist/proxy/request-processor.js +94 -0
  35. package/dist/proxy/response-processor.d.ts +14 -0
  36. package/dist/proxy/response-processor.js +128 -0
  37. package/dist/proxy/server.d.ts +9 -0
  38. package/dist/proxy/server.js +911 -0
  39. package/package.json +10 -4
@@ -1,5 +1,5 @@
1
1
  import type { ParsedSession } from './jsonl-parser.js';
2
- import type { TaskStatus } from './store.js';
2
+ import type { TaskStatus, SessionState, StepRecord } from './store.js';
3
3
  export interface ExtractedReasoning {
4
4
  task: string;
5
5
  goal: string;
@@ -17,6 +17,23 @@ export interface ExtractedReasoning {
17
17
  * Check if LLM extraction is available (OpenAI API key set)
18
18
  */
19
19
  export declare function isLLMAvailable(): boolean;
20
+ export interface ExtractedIntent {
21
+ goal: string;
22
+ expected_scope: string[];
23
+ constraints: string[];
24
+ success_criteria?: string[];
25
+ keywords: string[];
26
+ }
27
+ /**
28
+ * Extract intent from first user prompt using Haiku
29
+ * Called once at session start to populate session_states
30
+ * Falls back to basic extraction if API unavailable (for hook compatibility)
31
+ */
32
+ export declare function extractIntent(firstPrompt: string): Promise<ExtractedIntent>;
33
+ /**
34
+ * Check if intent extraction is available
35
+ */
36
+ export declare function isIntentExtractionAvailable(): boolean;
20
37
  /**
21
38
  * Check if Anthropic API is available (for drift detection)
22
39
  */
@@ -34,17 +51,49 @@ export declare function extractReasoning(session: ParsedSession): Promise<Extrac
34
51
  */
35
52
  export declare function classifyTaskStatus(session: ParsedSession): Promise<TaskStatus>;
36
53
  /**
37
- * Extracted intent from first prompt
54
+ * Check if session summary generation is available
38
55
  */
39
- export interface ExtractedIntent {
40
- goal: string;
41
- expected_scope: string[];
42
- constraints: string[];
43
- success_criteria: string[];
44
- keywords: string[];
56
+ export declare function isSummaryAvailable(): boolean;
57
+ /**
58
+ * Generate session summary for CLEAR operation
59
+ * Reference: plan_proxy_local.md Section 2.3, 4.5
60
+ */
61
+ export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[]): Promise<string>;
62
+ /**
63
+ * Task analysis result from Haiku
64
+ */
65
+ export interface TaskAnalysis {
66
+ action: 'continue' | 'new_task' | 'subtask' | 'parallel_task' | 'task_complete' | 'subtask_complete';
67
+ topic_match?: 'YES' | 'NO';
68
+ task_id: string;
69
+ current_goal: string;
70
+ parent_task_id?: string;
71
+ reasoning: string;
72
+ step_reasoning?: string;
45
73
  }
46
74
  /**
47
- * Extract intent from a prompt using Claude Haiku
48
- * Falls back to basic extraction if API unavailable
75
+ * Check if task analysis is available
76
+ */
77
+ export declare function isTaskAnalysisAvailable(): boolean;
78
+ /**
79
+ * Analyze task context to determine task status
80
+ * Called after each main model response to orchestrate sessions
81
+ * Also compresses reasoning for steps if assistantResponse > 1000 chars
82
+ */
83
+ export declare function analyzeTaskContext(currentSession: SessionState | null, latestUserMessage: string, recentSteps: StepRecord[], assistantResponse: string): Promise<TaskAnalysis>;
84
+ export interface ExtractedReasoningAndDecisions {
85
+ reasoning_trace: string[];
86
+ decisions: Array<{
87
+ choice: string;
88
+ reason: string;
89
+ }>;
90
+ }
91
+ /**
92
+ * Check if reasoning extraction is available
93
+ */
94
+ export declare function isReasoningExtractionAvailable(): boolean;
95
+ /**
96
+ * Extract reasoning trace and decisions from steps
97
+ * Called at task_complete to populate team memory with rich context
49
98
  */
50
- export declare function extractIntent(prompt: string): Promise<ExtractedIntent>;
99
+ export declare function extractReasoningAndDecisions(stepsReasoning: string[], originalGoal: string): Promise<ExtractedReasoningAndDecisions>;
@@ -2,8 +2,18 @@
2
2
  // and Anthropic Claude Haiku for drift detection
3
3
  import OpenAI from 'openai';
4
4
  import Anthropic from '@anthropic-ai/sdk';
5
+ import { config } from 'dotenv';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
8
+ import { existsSync } from 'fs';
5
9
  import { debugLLM } from './debug.js';
6
10
  import { truncate } from './utils.js';
11
+ // Load ~/.grov/.env as fallback for API key
12
+ // This allows users to store their API key in a safe location outside any repo
13
+ const grovEnvPath = join(homedir(), '.grov', '.env');
14
+ if (existsSync(grovEnvPath)) {
15
+ config({ path: grovEnvPath });
16
+ }
7
17
  let client = null;
8
18
  let anthropicClient = null;
9
19
  /**
@@ -39,6 +49,133 @@ function getAnthropicClient() {
39
49
  export function isLLMAvailable() {
40
50
  return !!process.env.OPENAI_API_KEY;
41
51
  }
52
+ /**
53
+ * Extract intent from first user prompt using Haiku
54
+ * Called once at session start to populate session_states
55
+ * Falls back to basic extraction if API unavailable (for hook compatibility)
56
+ */
57
+ export async function extractIntent(firstPrompt) {
58
+ // Check availability first - allows hook to work without API key
59
+ if (!isIntentExtractionAvailable()) {
60
+ return createFallbackIntent(firstPrompt);
61
+ }
62
+ try {
63
+ const client = getAnthropicClient();
64
+ const prompt = `Analyze this user request and extract structured intent for a coding assistant session.
65
+
66
+ USER REQUEST:
67
+ ${firstPrompt.substring(0, 2000)}
68
+
69
+ Extract as JSON:
70
+ {
71
+ "goal": "The main objective in 1-2 sentences",
72
+ "expected_scope": ["list", "of", "files/folders", "likely", "to", "be", "modified"],
73
+ "constraints": ["EXPLICIT restrictions from the user - see examples below"],
74
+ "success_criteria": ["How to know when the task is complete"],
75
+ "keywords": ["relevant", "technical", "terms"]
76
+ }
77
+
78
+ ═══════════════════════════════════════════════════════════════
79
+ CONSTRAINTS EXTRACTION - BE VERY THOROUGH
80
+ ═══════════════════════════════════════════════════════════════
81
+
82
+ Look for NEGATIVE constraints (things NOT to do):
83
+ - "NU modifica" / "DON'T modify" / "NEVER change" / "don't touch"
84
+ - "NU rula" / "DON'T run" / "NO commands" / "don't execute"
85
+ - "fără X" / "without X" / "except X" / "not including"
86
+ - "nu scrie cod" / "don't write code" / "just plan"
87
+
88
+ Look for POSITIVE constraints (things MUST do / ONLY do):
89
+ - "ONLY modify X" / "DOAR în X" / "only in folder Y"
90
+ - "must use Y" / "trebuie să folosești Y"
91
+ - "keep it simple" / "no external dependencies"
92
+ - "use TypeScript" / "must be async"
93
+
94
+ EXAMPLES:
95
+ Input: "Fix bug in auth. NU modifica nimic in afara de sandbox/, NU rula comenzi."
96
+ Output constraints: ["DO NOT modify files outside sandbox/", "DO NOT run commands"]
97
+
98
+ Input: "Add feature X. Only use standard library, keep backward compatible."
99
+ Output constraints: ["ONLY use standard library", "Keep backward compatible"]
100
+
101
+ Input: "Analyze code and create plan. Nu scrie cod inca, doar planifica."
102
+ Output constraints: ["DO NOT write code yet", "Only create plan/analysis"]
103
+
104
+ For expected_scope:
105
+ - Include file patterns (e.g., "src/auth/", "*.test.ts", "sandbox/")
106
+ - Include component/module names mentioned
107
+ - Be conservative - only include clearly relevant areas
108
+
109
+ RESPONSE RULES:
110
+ - English only (translate Romanian/other languages to English)
111
+ - No emojis
112
+ - Valid JSON only
113
+ - If no constraints found, return empty array []`;
114
+ const response = await client.messages.create({
115
+ model: 'claude-haiku-4-5-20251001',
116
+ max_tokens: 500,
117
+ messages: [{ role: 'user', content: prompt }],
118
+ });
119
+ const content = response.content?.[0];
120
+ if (!content || content.type !== 'text') {
121
+ return createFallbackIntent(firstPrompt);
122
+ }
123
+ try {
124
+ const jsonMatch = content.text.match(/\{[\s\S]*\}/);
125
+ if (!jsonMatch) {
126
+ return createFallbackIntent(firstPrompt);
127
+ }
128
+ const parsed = JSON.parse(jsonMatch[0]);
129
+ return {
130
+ goal: typeof parsed.goal === 'string' ? parsed.goal : firstPrompt.substring(0, 200),
131
+ expected_scope: Array.isArray(parsed.expected_scope)
132
+ ? parsed.expected_scope.filter((s) => typeof s === 'string')
133
+ : [],
134
+ constraints: Array.isArray(parsed.constraints)
135
+ ? parsed.constraints.filter((c) => typeof c === 'string')
136
+ : [],
137
+ success_criteria: Array.isArray(parsed.success_criteria)
138
+ ? parsed.success_criteria.filter((s) => typeof s === 'string')
139
+ : [],
140
+ keywords: Array.isArray(parsed.keywords)
141
+ ? parsed.keywords.filter((k) => typeof k === 'string')
142
+ : [],
143
+ };
144
+ }
145
+ catch {
146
+ return createFallbackIntent(firstPrompt);
147
+ }
148
+ }
149
+ catch {
150
+ // Outer catch - API errors, network issues, etc.
151
+ return createFallbackIntent(firstPrompt);
152
+ }
153
+ }
154
+ /**
155
+ * Fallback intent extraction without LLM
156
+ */
157
+ function createFallbackIntent(prompt) {
158
+ // Basic keyword extraction
159
+ const words = prompt.toLowerCase().split(/\s+/);
160
+ const techKeywords = words.filter(w => w.length > 3 &&
161
+ /^[a-z]+$/.test(w) &&
162
+ !['this', 'that', 'with', 'from', 'have', 'will', 'would', 'could', 'should'].includes(w));
163
+ // Extract file patterns
164
+ const filePatterns = prompt.match(/[\w\/.-]+\.(ts|js|tsx|jsx|py|go|rs|java|css|html|md)/g) || [];
165
+ return {
166
+ goal: prompt.substring(0, 200),
167
+ expected_scope: [...new Set(filePatterns)].slice(0, 5),
168
+ constraints: [],
169
+ success_criteria: [],
170
+ keywords: [...new Set(techKeywords)].slice(0, 10),
171
+ };
172
+ }
173
+ /**
174
+ * Check if intent extraction is available
175
+ */
176
+ export function isIntentExtractionAvailable() {
177
+ return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
178
+ }
42
179
  /**
43
180
  * Check if Anthropic API is available (for drift detection)
44
181
  */
@@ -75,22 +212,37 @@ ${sessionSummary}
75
212
 
76
213
  Extract the following as JSON:
77
214
  {
78
- "task": "Brief description of what the user was trying to do (1 sentence)",
79
- "goal": "The underlying goal or problem being solved",
80
- "reasoning_trace": ["Key reasoning steps taken", "Decisions made and why", "What was investigated"],
81
- "decisions": [{"choice": "What was decided", "reason": "Why this choice was made"}],
82
- "constraints": ["Any constraints or requirements discovered"],
215
+ "task": "Brief description (1 sentence)",
216
+ "goal": "The underlying problem being solved",
217
+ "reasoning_trace": [
218
+ "Be SPECIFIC: include file names, function names, line numbers when relevant",
219
+ "Format: '[Action] [target] to/for [purpose]'",
220
+ "Example: 'Read auth.ts:47 to understand token refresh logic'",
221
+ "Example: 'Fixed null check in validateToken() - was causing silent failures'",
222
+ "NOT: 'Investigated auth' or 'Fixed bug'"
223
+ ],
224
+ "decisions": [{"choice": "What was decided", "reason": "Why this over alternatives"}],
225
+ "constraints": ["Discovered limitations, rate limits, incompatibilities"],
83
226
  "status": "complete|partial|question|abandoned",
84
227
  "tags": ["relevant", "domain", "tags"]
85
228
  }
86
229
 
230
+ IMPORTANT for reasoning_trace:
231
+ - Each entry should be ACTIONABLE information for future developers
232
+ - Include specific file:line references when possible
233
+ - Explain WHY not just WHAT (e.g., "Chose JWT over sessions because stateless scales better")
234
+ - Bad: "Fixed the bug" / Good: "Fixed race condition in UserService.save() - was missing await"
235
+
87
236
  Status definitions:
88
237
  - "complete": Task was finished, implementation done
89
238
  - "partial": Work started but not finished
90
239
  - "question": Claude asked a question and is waiting for user response
91
240
  - "abandoned": User interrupted or moved to different topic
92
241
 
93
- Return ONLY valid JSON, no explanation.`
242
+ RESPONSE RULES:
243
+ - English only (translate if input is in other language)
244
+ - No emojis
245
+ - Valid JSON only`
94
246
  }
95
247
  ]
96
248
  });
@@ -290,119 +442,288 @@ function validateStatus(status) {
290
442
  }
291
443
  return 'partial'; // Default
292
444
  }
445
+ // ============================================
446
+ // SESSION SUMMARY FOR CLEAR OPERATION
447
+ // Reference: plan_proxy_local.md Section 2.3, 4.5
448
+ // ============================================
293
449
  /**
294
- * Extract intent from a prompt using Claude Haiku
295
- * Falls back to basic extraction if API unavailable
450
+ * Check if session summary generation is available
296
451
  */
297
- export async function extractIntent(prompt) {
298
- // Try LLM extraction if available
299
- if (isAnthropicAvailable()) {
300
- try {
301
- return await extractIntentWithLLM(prompt);
452
+ export function isSummaryAvailable() {
453
+ return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
454
+ }
455
+ /**
456
+ * Generate session summary for CLEAR operation
457
+ * Reference: plan_proxy_local.md Section 2.3, 4.5
458
+ */
459
+ export async function generateSessionSummary(sessionState, steps) {
460
+ const client = getAnthropicClient();
461
+ const stepsText = steps
462
+ .filter(s => s.is_validated)
463
+ .slice(-20)
464
+ .map(step => {
465
+ let desc = `- ${step.action_type}`;
466
+ if (step.files.length > 0) {
467
+ desc += `: ${step.files.join(', ')}`;
302
468
  }
303
- catch (error) {
304
- debugLLM('extractIntent LLM failed, using fallback: %O', error);
305
- return extractIntentBasic(prompt);
469
+ if (step.command) {
470
+ desc += ` (${step.command.substring(0, 50)})`;
306
471
  }
472
+ return desc;
473
+ })
474
+ .join('\n');
475
+ const prompt = `Create a concise summary of this coding session for context continuation.
476
+
477
+ ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
478
+
479
+ EXPECTED SCOPE: ${sessionState.expected_scope.join(', ') || 'Not specified'}
480
+
481
+ CONSTRAINTS: ${sessionState.constraints.join(', ') || 'None'}
482
+
483
+ ACTIONS TAKEN:
484
+ ${stepsText || 'No actions recorded'}
485
+
486
+ Create a summary with these sections (keep total under 500 words):
487
+ 1. ORIGINAL GOAL: (1 sentence)
488
+ 2. PROGRESS: (2-3 bullet points of what was accomplished)
489
+ 3. KEY DECISIONS: (any important choices made)
490
+ 4. FILES MODIFIED: (list of files)
491
+ 5. CURRENT STATE: (where the work left off)
492
+ 6. NEXT STEPS: (recommended next actions)
493
+
494
+ Format as plain text, not JSON.`;
495
+ const response = await client.messages.create({
496
+ model: 'claude-haiku-4-5-20251001',
497
+ max_tokens: 800,
498
+ messages: [{ role: 'user', content: prompt }],
499
+ });
500
+ const content = response.content?.[0];
501
+ if (!content || content.type !== 'text') {
502
+ return createFallbackSummary(sessionState, steps);
307
503
  }
308
- // Fallback to basic extraction
309
- return extractIntentBasic(prompt);
504
+ return `PREVIOUS SESSION CONTEXT (auto-generated after context limit):
505
+
506
+ ${content.text}`;
310
507
  }
311
508
  /**
312
- * Extract intent using Claude Haiku
509
+ * Create fallback summary without LLM
313
510
  */
314
- async function extractIntentWithLLM(prompt) {
315
- const anthropic = getAnthropicClient();
316
- const model = getDriftModel();
317
- const response = await anthropic.messages.create({
318
- model,
319
- max_tokens: 1024,
320
- messages: [
321
- {
322
- role: 'user',
323
- content: `Analyze this user prompt and extract the task intent. Return ONLY valid JSON, no explanation.
511
+ function createFallbackSummary(sessionState, steps) {
512
+ const files = [...new Set(steps.flatMap(s => s.files))];
513
+ return `PREVIOUS SESSION CONTEXT (auto-generated after context limit):
324
514
 
325
- USER PROMPT:
326
- ${prompt}
515
+ ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
327
516
 
328
- Extract as JSON:
517
+ PROGRESS: ${steps.length} actions taken
518
+
519
+ FILES MODIFIED:
520
+ ${files.slice(0, 10).map(f => `- ${f}`).join('\n') || '- None recorded'}
521
+
522
+ Please continue from where you left off.`;
523
+ }
524
+ /**
525
+ * Check if task analysis is available
526
+ */
527
+ export function isTaskAnalysisAvailable() {
528
+ return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
529
+ }
530
+ /**
531
+ * Analyze task context to determine task status
532
+ * Called after each main model response to orchestrate sessions
533
+ * Also compresses reasoning for steps if assistantResponse > 1000 chars
534
+ */
535
+ export async function analyzeTaskContext(currentSession, latestUserMessage, recentSteps, assistantResponse) {
536
+ const client = getAnthropicClient();
537
+ const stepsText = recentSteps.slice(0, 5).map(s => {
538
+ let desc = `- ${s.action_type}`;
539
+ if (s.files.length > 0) {
540
+ desc += `: ${s.files.slice(0, 3).join(', ')}`;
541
+ }
542
+ return desc;
543
+ }).join('\n') || 'None';
544
+ // Check if we need to compress reasoning
545
+ const needsCompression = assistantResponse.length > 1000;
546
+ const compressionInstruction = needsCompression
547
+ ? `\n "step_reasoning": "compressed summary of assistant's actions and reasoning (max 800 chars)"`
548
+ : '';
549
+ const compressionRule = needsCompression
550
+ ? '\n- step_reasoning: Summarize what the assistant did and WHY in a concise way (max 800 chars)'
551
+ : '';
552
+ // Extract topic keywords from goal for comparison
553
+ const currentGoalKeywords = currentSession?.original_goal
554
+ ? currentSession.original_goal.toLowerCase().match(/\b\w{4,}\b/g)?.slice(0, 10).join(', ') || ''
555
+ : '';
556
+ const prompt = `You are a task orchestrator. Your PRIMARY job is to detect when the user starts a NEW, DIFFERENT task.
557
+
558
+ CURRENT SESSION:
559
+ - Current Goal: "${currentSession?.original_goal || 'No active task'}"
560
+ - Goal Keywords: [${currentGoalKeywords}]
561
+
562
+ LATEST USER MESSAGE:
563
+ "${latestUserMessage.substring(0, 500)}"
564
+
565
+ RECENT ACTIONS (last 5):
566
+ ${stepsText}
567
+
568
+ ASSISTANT RESPONSE (truncated):
569
+ "${assistantResponse.substring(0, 1500)}${assistantResponse.length > 1500 ? '...' : ''}"
570
+
571
+ ═══════════════════════════════════════════════════════════════
572
+ CRITICAL: Compare the TOPIC of "Current Goal" vs "Latest User Message"
573
+ ═══════════════════════════════════════════════════════════════
574
+
575
+ Ask yourself:
576
+ 1. Is the user message about the SAME subject/feature/file as the current goal?
577
+ 2. Or is it about something COMPLETELY DIFFERENT?
578
+
579
+ EXAMPLES of NEW_TASK (different topic):
580
+ - Goal: "implement authentication" → User: "fix the database migration" → NEW_TASK
581
+ - Goal: "analyze security layer" → User: "create hello.ts script" → NEW_TASK
582
+ - Goal: "refactor user service" → User: "add dark mode to UI" → NEW_TASK
583
+ - Goal: "fix login bug" → User: "write unit tests for payments" → NEW_TASK
584
+
585
+ EXAMPLES of CONTINUE (same topic):
586
+ - Goal: "implement authentication" → User: "now add the logout button" → CONTINUE
587
+ - Goal: "fix login bug" → User: "also check the session timeout" → CONTINUE
588
+ - Goal: "analyze security" → User: "what about rate limiting?" → CONTINUE
589
+
590
+ Return JSON:
329
591
  {
330
- "goal": "The main objective the user wants to achieve (1 sentence)",
331
- "expected_scope": ["List of files, directories, or components that should be touched"],
332
- "constraints": ["Any constraints or requirements mentioned"],
333
- "success_criteria": ["How to know when the task is complete"],
334
- "keywords": ["Important technical terms from the prompt"]
592
+ "action": "continue|new_task|subtask|parallel_task|task_complete|subtask_complete",
593
+ "topic_match": "YES if same topic, NO if different topic",
594
+ "task_id": "existing session_id or 'NEW' for new task",
595
+ "current_goal": "the goal based on LATEST user message",
596
+ "reasoning": "1 sentence explaining topic comparison"${compressionInstruction}
335
597
  }
336
598
 
337
- Return ONLY valid JSON.`
338
- }
339
- ]
599
+ DECISION RULES:
600
+ 1. NO current session → "new_task"
601
+ 2. topic_match=NO (different subject) → "new_task"
602
+ 3. topic_match=YES + user following up → "continue"
603
+ 4. Claude said "done/complete/finished" → "task_complete"
604
+ 5. Prerequisite work identified → "subtask"${compressionRule}
605
+
606
+ RESPONSE RULES:
607
+ - English only (translate if input is in other language)
608
+ - No emojis
609
+ - Valid JSON only`;
610
+ debugLLM('analyzeTaskContext', `Calling Haiku for task analysis (needsCompression=${needsCompression})`);
611
+ const response = await client.messages.create({
612
+ model: 'claude-haiku-4-5-20251001',
613
+ max_tokens: needsCompression ? 600 : 300,
614
+ messages: [{ role: 'user', content: prompt }],
340
615
  });
341
- // Extract text content from response
342
- const content = response.content[0];
343
- if (content.type !== 'text') {
344
- throw new Error('Unexpected response type from Anthropic');
616
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
617
+ try {
618
+ // Try to parse JSON from response (may have extra text)
619
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
620
+ if (!jsonMatch) {
621
+ throw new Error('No JSON found in response');
622
+ }
623
+ const analysis = JSON.parse(jsonMatch[0]);
624
+ // If we didn't need compression but have short response, use it directly
625
+ if (!needsCompression && assistantResponse.length > 0) {
626
+ analysis.step_reasoning = assistantResponse.substring(0, 1000);
627
+ }
628
+ debugLLM('analyzeTaskContext', `Result: action=${analysis.action}, topic_match=${analysis.topic_match}, goal=${analysis.current_goal.substring(0, 50)}`);
629
+ return analysis;
630
+ }
631
+ catch (parseError) {
632
+ debugLLM('analyzeTaskContext', `Parse error: ${String(parseError)}, using fallback`);
633
+ // Fallback: continue existing session or create new
634
+ return {
635
+ action: currentSession ? 'continue' : 'new_task',
636
+ task_id: currentSession?.session_id || 'NEW',
637
+ current_goal: latestUserMessage.substring(0, 200),
638
+ reasoning: 'Fallback due to parse error',
639
+ step_reasoning: assistantResponse.substring(0, 1000),
640
+ };
345
641
  }
346
- const parsed = JSON.parse(content.text);
347
- return {
348
- goal: parsed.goal || prompt.substring(0, 100),
349
- expected_scope: parsed.expected_scope || [],
350
- constraints: parsed.constraints || [],
351
- success_criteria: parsed.success_criteria || [],
352
- keywords: parsed.keywords || extractKeywordsBasic(prompt)
353
- };
354
642
  }
355
643
  /**
356
- * Basic intent extraction without LLM
644
+ * Check if reasoning extraction is available
357
645
  */
358
- function extractIntentBasic(prompt) {
359
- return {
360
- goal: prompt.substring(0, 200),
361
- expected_scope: extractFilesFromPrompt(prompt),
362
- constraints: [],
363
- success_criteria: [],
364
- keywords: extractKeywordsBasic(prompt)
365
- };
646
+ export function isReasoningExtractionAvailable() {
647
+ return !!process.env.ANTHROPIC_API_KEY || !!process.env.GROV_API_KEY;
366
648
  }
367
649
  /**
368
- * Extract file paths from prompt text
650
+ * Extract reasoning trace and decisions from steps
651
+ * Called at task_complete to populate team memory with rich context
369
652
  */
370
- function extractFilesFromPrompt(prompt) {
371
- const patterns = [
372
- /(?:^|\s)(\/[\w\-\.\/]+\.\w+)/g,
373
- /(?:^|\s)(\.\/[\w\-\.\/]+\.\w+)/g,
374
- /(?:^|\s)([\w\-]+\/[\w\-\.\/]+\.\w+)/g,
375
- /(?:^|\s|['"`])([\w\-]+\.\w{1,5})(?:\s|$|,|:|['"`])/g,
376
- ];
377
- const files = new Set();
378
- for (const pattern of patterns) {
379
- const matches = prompt.matchAll(pattern);
380
- for (const match of matches) {
381
- const file = match[1].trim();
382
- if (file && !file.match(/^(http|https|ftp|mailto)/) && !file.match(/^\d+\.\d+/)) {
383
- files.add(file);
384
- }
385
- }
653
+ export async function extractReasoningAndDecisions(stepsReasoning, originalGoal) {
654
+ const client = getAnthropicClient();
655
+ // Combine all steps reasoning into one text
656
+ const combinedReasoning = stepsReasoning
657
+ .filter(r => r && r.length > 10)
658
+ .join('\n\n---\n\n')
659
+ .substring(0, 8000);
660
+ if (combinedReasoning.length < 50) {
661
+ return { reasoning_trace: [], decisions: [] };
386
662
  }
387
- return [...files];
663
+ const prompt = `Analyze Claude's work session and extract structured reasoning and decisions.
664
+
665
+ ORIGINAL GOAL:
666
+ ${originalGoal || 'Not specified'}
667
+
668
+ CLAUDE'S WORK (reasoning from each step):
669
+ ${combinedReasoning}
670
+
671
+ ═══════════════════════════════════════════════════════════════
672
+ EXTRACT TWO THINGS:
673
+ ═══════════════════════════════════════════════════════════════
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"
685
+ - Max 5 decisions
686
+
687
+ Return JSON:
688
+ {
689
+ "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"
693
+ ],
694
+ "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"}
697
+ ]
388
698
  }
389
- /**
390
- * Extract keywords from prompt (basic)
391
- */
392
- function extractKeywordsBasic(prompt) {
393
- const stopWords = new Set([
394
- 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
395
- 'to', 'for', 'and', 'or', 'in', 'on', 'at', 'of', 'with',
396
- 'this', 'that', 'it', 'i', 'you', 'we', 'they', 'my', 'your',
397
- 'can', 'could', 'would', 'should', 'will', 'do', 'does', 'did',
398
- 'have', 'has', 'had', 'not', 'but', 'if', 'then', 'when', 'where',
399
- 'how', 'what', 'why', 'which', 'who', 'all', 'some', 'any', 'no',
400
- 'from', 'by', 'as', 'so', 'too', 'also', 'just', 'only', 'now',
401
- 'please', 'help', 'me', 'make', 'get', 'add', 'fix', 'update', 'change'
402
- ]);
403
- const words = prompt.toLowerCase()
404
- .replace(/[^\w\s]/g, ' ')
405
- .split(/\s+/)
406
- .filter(w => w.length > 2 && !stopWords.has(w));
407
- return [...new Set(words)].slice(0, 15);
699
+
700
+ RESPONSE RULES:
701
+ - English only
702
+ - No emojis
703
+ - Valid JSON only
704
+ - Be specific, not vague (bad: "Fixed bug", good: "Fixed null check in validateToken")`;
705
+ debugLLM('extractReasoningAndDecisions', `Analyzing ${stepsReasoning.length} steps, ${combinedReasoning.length} chars`);
706
+ try {
707
+ const response = await client.messages.create({
708
+ model: 'claude-haiku-4-5-20251001',
709
+ max_tokens: 800,
710
+ messages: [{ role: 'user', content: prompt }],
711
+ });
712
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
713
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
714
+ if (!jsonMatch) {
715
+ debugLLM('extractReasoningAndDecisions', 'No JSON found in response');
716
+ return { reasoning_trace: [], decisions: [] };
717
+ }
718
+ const result = JSON.parse(jsonMatch[0]);
719
+ debugLLM('extractReasoningAndDecisions', `Extracted ${result.reasoning_trace?.length || 0} traces, ${result.decisions?.length || 0} decisions`);
720
+ return {
721
+ reasoning_trace: result.reasoning_trace || [],
722
+ decisions: result.decisions || [],
723
+ };
724
+ }
725
+ catch (error) {
726
+ debugLLM('extractReasoningAndDecisions', `Error: ${String(error)}`);
727
+ return { reasoning_trace: [], decisions: [] };
728
+ }
408
729
  }