grov 0.1.0

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/LICENSE +190 -0
  2. package/README.md +211 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +106 -0
  5. package/dist/commands/capture.d.ts +6 -0
  6. package/dist/commands/capture.js +324 -0
  7. package/dist/commands/drift-test.d.ts +7 -0
  8. package/dist/commands/drift-test.js +177 -0
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +27 -0
  11. package/dist/commands/inject.d.ts +5 -0
  12. package/dist/commands/inject.js +88 -0
  13. package/dist/commands/prompt-inject.d.ts +4 -0
  14. package/dist/commands/prompt-inject.js +451 -0
  15. package/dist/commands/status.d.ts +5 -0
  16. package/dist/commands/status.js +51 -0
  17. package/dist/commands/unregister.d.ts +1 -0
  18. package/dist/commands/unregister.js +22 -0
  19. package/dist/lib/anchor-extractor.d.ts +30 -0
  20. package/dist/lib/anchor-extractor.js +296 -0
  21. package/dist/lib/correction-builder.d.ts +10 -0
  22. package/dist/lib/correction-builder.js +226 -0
  23. package/dist/lib/debug.d.ts +24 -0
  24. package/dist/lib/debug.js +34 -0
  25. package/dist/lib/drift-checker.d.ts +66 -0
  26. package/dist/lib/drift-checker.js +341 -0
  27. package/dist/lib/hooks.d.ts +27 -0
  28. package/dist/lib/hooks.js +258 -0
  29. package/dist/lib/jsonl-parser.d.ts +87 -0
  30. package/dist/lib/jsonl-parser.js +281 -0
  31. package/dist/lib/llm-extractor.d.ts +50 -0
  32. package/dist/lib/llm-extractor.js +408 -0
  33. package/dist/lib/session-parser.d.ts +44 -0
  34. package/dist/lib/session-parser.js +256 -0
  35. package/dist/lib/store.d.ts +248 -0
  36. package/dist/lib/store.js +793 -0
  37. package/dist/lib/utils.d.ts +31 -0
  38. package/dist/lib/utils.js +76 -0
  39. package/package.json +67 -0
@@ -0,0 +1,296 @@
1
+ // Extract function/class anchors from source files for file-level reasoning
2
+ import { createHash } from 'crypto';
3
+ import { extname } from 'path';
4
+ const TYPESCRIPT_PATTERNS = {
5
+ // export async function foo() or function foo()
6
+ function: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
7
+ // export const foo = async () => or const foo = function()
8
+ arrowFunction: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)\s*=>|\([^)]*\)\s*:\s*\w+\s*=>|function)/,
9
+ // export class Foo or class Foo
10
+ class: /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/,
11
+ // async foo() { or foo(): Promise<void> { or private foo() {
12
+ method: /^\s+(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*(?:<[^>]*>)?\s*\([^)]*\)/,
13
+ };
14
+ const PYTHON_PATTERNS = {
15
+ // def foo():
16
+ function: /^def\s+(\w+)\s*\(/,
17
+ // class Foo:
18
+ class: /^class\s+(\w+)/,
19
+ // def foo(self): (indented method)
20
+ method: /^\s+def\s+(\w+)\s*\(/,
21
+ };
22
+ const GO_PATTERNS = {
23
+ // func foo() or func (r *Receiver) foo()
24
+ function: /^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/,
25
+ // type Foo struct
26
+ class: /^type\s+(\w+)\s+struct/,
27
+ // Method receivers are handled by function pattern
28
+ method: /^func\s+\([^)]+\)\s+(\w+)\s*\(/,
29
+ };
30
+ const RUST_PATTERNS = {
31
+ // fn foo() or pub fn foo()
32
+ function: /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
33
+ // struct Foo or pub struct Foo
34
+ class: /^(?:pub\s+)?struct\s+(\w+)/,
35
+ // fn foo(&self) inside impl block (indented)
36
+ method: /^\s+(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
37
+ };
38
+ /**
39
+ * Get language patterns based on file extension
40
+ */
41
+ function getPatternsForFile(filePath) {
42
+ const ext = extname(filePath).toLowerCase();
43
+ switch (ext) {
44
+ case '.ts':
45
+ case '.tsx':
46
+ case '.js':
47
+ case '.jsx':
48
+ case '.mjs':
49
+ case '.cjs':
50
+ return TYPESCRIPT_PATTERNS;
51
+ case '.py':
52
+ return PYTHON_PATTERNS;
53
+ case '.go':
54
+ return GO_PATTERNS;
55
+ case '.rs':
56
+ return RUST_PATTERNS;
57
+ default:
58
+ return null;
59
+ }
60
+ }
61
+ // PERFORMANCE: Maximum anchors per file to prevent DoS
62
+ const MAX_ANCHORS_PER_FILE = 1000;
63
+ // PERFORMANCE: Maximum file size to process (1MB)
64
+ const MAX_FILE_SIZE = 1024 * 1024;
65
+ /**
66
+ * Extract all anchors from a source file.
67
+ * PERFORMANCE: Uses single-pass O(n) algorithm instead of O(n²).
68
+ * SECURITY: Limits anchors to prevent DoS with pathological files.
69
+ */
70
+ export function extractAnchors(filePath, content) {
71
+ // SECURITY: Skip files that are too large
72
+ if (content.length > MAX_FILE_SIZE) {
73
+ return [];
74
+ }
75
+ const patterns = getPatternsForFile(filePath);
76
+ if (!patterns) {
77
+ return [];
78
+ }
79
+ const lines = content.split('\n');
80
+ const isPython = filePath.endsWith('.py');
81
+ const anchors = [];
82
+ const openAnchors = [];
83
+ let currentBraceDepth = 0;
84
+ for (let i = 0; i < lines.length; i++) {
85
+ // SECURITY: Stop if we've found too many anchors
86
+ if (anchors.length >= MAX_ANCHORS_PER_FILE) {
87
+ break;
88
+ }
89
+ const line = lines[i];
90
+ const lineNumber = i + 1; // 1-indexed
91
+ // Count braces in this line BEFORE pattern matching
92
+ let lineOpenBraces = 0;
93
+ let lineCloseBraces = 0;
94
+ for (const char of line) {
95
+ if (char === '{')
96
+ lineOpenBraces++;
97
+ else if (char === '}')
98
+ lineCloseBraces++;
99
+ }
100
+ // Close any open anchors whose braces have returned to their start level
101
+ // Do this BEFORE adding line's braces to handle same-line open/close
102
+ if (!isPython) {
103
+ // Process closing braces
104
+ for (let b = 0; b < lineCloseBraces; b++) {
105
+ currentBraceDepth--;
106
+ // Check if any open anchor should be closed
107
+ while (openAnchors.length > 0) {
108
+ const top = openAnchors[openAnchors.length - 1];
109
+ if (currentBraceDepth < top.braceDepthAtStart) {
110
+ openAnchors.pop();
111
+ anchors.push({
112
+ type: top.type,
113
+ name: top.name,
114
+ lineStart: top.lineStart,
115
+ lineEnd: lineNumber,
116
+ });
117
+ }
118
+ else {
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ else {
125
+ // Python: Check indentation-based closing
126
+ const currentIndent = line.match(/^(\s*)/)?.[1].length || 0;
127
+ // Skip empty lines for indentation checking
128
+ if (line.trim() && !line.trim().startsWith('#')) {
129
+ while (openAnchors.length > 0) {
130
+ const top = openAnchors[openAnchors.length - 1];
131
+ if (top.baseIndent !== undefined && currentIndent <= top.baseIndent) {
132
+ openAnchors.pop();
133
+ anchors.push({
134
+ type: top.type,
135
+ name: top.name,
136
+ lineStart: top.lineStart,
137
+ lineEnd: lineNumber - 1, // End at previous line
138
+ });
139
+ }
140
+ else {
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ // Pattern matching - find new anchors
147
+ let match = null;
148
+ let anchorType = null;
149
+ // Check for function
150
+ match = line.match(patterns.function);
151
+ if (match) {
152
+ anchorType = 'function';
153
+ }
154
+ // Check for arrow function (TypeScript/JavaScript)
155
+ if (!match && patterns.arrowFunction) {
156
+ match = line.match(patterns.arrowFunction);
157
+ if (match) {
158
+ anchorType = 'function';
159
+ }
160
+ }
161
+ // Check for class
162
+ if (!match) {
163
+ match = line.match(patterns.class);
164
+ if (match) {
165
+ anchorType = 'class';
166
+ }
167
+ }
168
+ // Check for method (only if indented)
169
+ if (!match && line.match(/^\s+/) && !line.match(/^\s*\/\//)) {
170
+ match = line.match(patterns.method);
171
+ if (match) {
172
+ anchorType = 'method';
173
+ }
174
+ }
175
+ // If we found a new anchor, add it to open anchors
176
+ if (match && anchorType) {
177
+ const baseIndent = isPython ? (line.match(/^(\s*)/)?.[1].length || 0) : undefined;
178
+ openAnchors.push({
179
+ type: anchorType,
180
+ name: match[1],
181
+ lineStart: lineNumber,
182
+ braceDepthAtStart: currentBraceDepth + lineOpenBraces,
183
+ baseIndent,
184
+ });
185
+ }
186
+ // Update brace depth with opening braces
187
+ if (!isPython) {
188
+ currentBraceDepth += lineOpenBraces;
189
+ }
190
+ }
191
+ // Close any remaining open anchors at end of file
192
+ for (const open of openAnchors) {
193
+ anchors.push({
194
+ type: open.type,
195
+ name: open.name,
196
+ lineStart: open.lineStart,
197
+ lineEnd: lines.length,
198
+ });
199
+ }
200
+ return anchors;
201
+ }
202
+ /**
203
+ * Find the end of a code block (function/class/method body)
204
+ * Uses brace counting for C-like languages, indentation for Python
205
+ */
206
+ function findBlockEnd(lines, startIndex) {
207
+ const startLine = lines[startIndex];
208
+ const isIndentBased = !startLine.includes('{');
209
+ if (isIndentBased) {
210
+ // Python-style: find end by indentation
211
+ const baseIndent = startLine.match(/^(\s*)/)?.[1].length || 0;
212
+ for (let i = startIndex + 1; i < lines.length; i++) {
213
+ const line = lines[i];
214
+ // Skip empty lines and comments
215
+ if (!line.trim() || line.trim().startsWith('#'))
216
+ continue;
217
+ const currentIndent = line.match(/^(\s*)/)?.[1].length || 0;
218
+ if (currentIndent <= baseIndent && line.trim()) {
219
+ return i; // Previous line was the end
220
+ }
221
+ }
222
+ return lines.length;
223
+ }
224
+ // Brace-counting for C-like languages
225
+ let braceCount = 0;
226
+ let foundOpenBrace = false;
227
+ for (let i = startIndex; i < lines.length; i++) {
228
+ const line = lines[i];
229
+ for (const char of line) {
230
+ if (char === '{') {
231
+ braceCount++;
232
+ foundOpenBrace = true;
233
+ }
234
+ else if (char === '}') {
235
+ braceCount--;
236
+ }
237
+ }
238
+ if (foundOpenBrace && braceCount === 0) {
239
+ return i + 1; // 1-indexed
240
+ }
241
+ }
242
+ return lines.length;
243
+ }
244
+ /**
245
+ * Find which anchor contains a given line number
246
+ */
247
+ export function findAnchorAtLine(anchors, lineNumber) {
248
+ // Find the most specific (innermost) anchor that contains this line
249
+ let bestMatch = null;
250
+ for (const anchor of anchors) {
251
+ const end = anchor.lineEnd || anchor.lineStart;
252
+ if (lineNumber >= anchor.lineStart && lineNumber <= end) {
253
+ // Prefer more specific matches (methods over classes)
254
+ if (!bestMatch || anchor.lineStart > bestMatch.lineStart) {
255
+ bestMatch = anchor;
256
+ }
257
+ }
258
+ }
259
+ return bestMatch;
260
+ }
261
+ /**
262
+ * Compute a hash of a code region for change detection.
263
+ * Uses SHA-256 (truncated) for security scanner compliance.
264
+ */
265
+ export function computeCodeHash(content, lineStart, lineEnd) {
266
+ const lines = content.split('\n');
267
+ const slice = lines.slice(lineStart - 1, lineEnd).join('\n');
268
+ // Normalize whitespace for more stable hashes
269
+ const normalized = slice.replace(/\s+/g, ' ').trim();
270
+ // SECURITY: Use SHA-256 instead of MD5 (truncated for storage efficiency)
271
+ return createHash('sha256').update(normalized).digest('hex').substring(0, 16);
272
+ }
273
+ /**
274
+ * Estimate the line number where a string appears in content
275
+ */
276
+ export function estimateLineNumber(searchString, content) {
277
+ if (!searchString || !content)
278
+ return null;
279
+ // Get first line of search string for matching
280
+ const firstLine = searchString.split('\n')[0].trim();
281
+ if (!firstLine)
282
+ return null;
283
+ const lines = content.split('\n');
284
+ for (let i = 0; i < lines.length; i++) {
285
+ if (lines[i].includes(firstLine)) {
286
+ return i + 1; // 1-indexed
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+ /**
292
+ * Get a human-readable description of an anchor
293
+ */
294
+ export function describeAnchor(anchor) {
295
+ return `${anchor.type} "${anchor.name}" at line ${anchor.lineStart}${anchor.lineEnd ? `-${anchor.lineEnd}` : ''}`;
296
+ }
@@ -0,0 +1,10 @@
1
+ import type { DriftCheckResult } from './drift-checker.js';
2
+ import type { SessionState, CorrectionLevel } from './store.js';
3
+ /**
4
+ * Determine correction level from score and escalation count
5
+ */
6
+ export declare function determineCorrectionLevel(score: number, escalationCount: number): CorrectionLevel | null;
7
+ /**
8
+ * Build correction message based on level
9
+ */
10
+ export declare function buildCorrection(result: DriftCheckResult, sessionState: SessionState, level: CorrectionLevel): string;
@@ -0,0 +1,226 @@
1
+ // Correction message builder for anti-drift system
2
+ // Builds XML-tagged correction messages by severity level
3
+ import { DRIFT_CONFIG } from './drift-checker.js';
4
+ // ============================================
5
+ // MAIN FUNCTIONS
6
+ // ============================================
7
+ /**
8
+ * Determine correction level from score and escalation count
9
+ */
10
+ export function determineCorrectionLevel(score, escalationCount) {
11
+ // Apply escalation modifier (each escalation level lowers threshold by 1)
12
+ const effectiveScore = score - escalationCount;
13
+ if (effectiveScore >= DRIFT_CONFIG.SCORE_NO_INJECTION) {
14
+ return null; // No correction needed
15
+ }
16
+ if (effectiveScore >= DRIFT_CONFIG.SCORE_NUDGE) {
17
+ return 'nudge';
18
+ }
19
+ if (effectiveScore >= DRIFT_CONFIG.SCORE_CORRECT) {
20
+ return 'correct';
21
+ }
22
+ if (effectiveScore >= DRIFT_CONFIG.SCORE_INTERVENE) {
23
+ return 'intervene';
24
+ }
25
+ return 'halt';
26
+ }
27
+ /**
28
+ * Build correction message based on level
29
+ */
30
+ export function buildCorrection(result, sessionState, level) {
31
+ switch (level) {
32
+ case 'nudge':
33
+ return buildNudge(result, sessionState);
34
+ case 'correct':
35
+ return buildCorrect(result, sessionState);
36
+ case 'intervene':
37
+ return buildIntervene(result, sessionState);
38
+ case 'halt':
39
+ return buildHalt(result, sessionState);
40
+ }
41
+ }
42
+ // ============================================
43
+ // CORRECTION BUILDERS
44
+ // ============================================
45
+ /**
46
+ * NUDGE - Gentle 2-3 sentence reminder
47
+ * Score ~7, first sign of drift
48
+ */
49
+ function buildNudge(result, sessionState) {
50
+ const lines = [];
51
+ lines.push('<grov_nudge>');
52
+ lines.push('');
53
+ lines.push(`Reminder: Original goal is "${truncateGoal(sessionState.original_goal)}".`);
54
+ lines.push(`Current alignment: ${result.score}/10.`);
55
+ if (result.diagnostic) {
56
+ lines.push(`Note: ${result.diagnostic}`);
57
+ }
58
+ lines.push('');
59
+ lines.push('</grov_nudge>');
60
+ return lines.join('\n');
61
+ }
62
+ /**
63
+ * CORRECT - Deviation + scope + next steps
64
+ * Score 5-6, moderate drift
65
+ */
66
+ function buildCorrect(result, sessionState) {
67
+ const lines = [];
68
+ lines.push('<grov_correction>');
69
+ lines.push('');
70
+ lines.push('DRIFT DETECTED');
71
+ lines.push('');
72
+ lines.push(`Original goal: "${truncateGoal(sessionState.original_goal)}"`);
73
+ lines.push(`Current alignment: ${result.score}/10`);
74
+ lines.push(`Diagnostic: ${result.diagnostic}`);
75
+ lines.push('');
76
+ // Add scope reminder
77
+ if (sessionState.expected_scope.length > 0) {
78
+ lines.push('Expected scope:');
79
+ for (const scope of sessionState.expected_scope.slice(0, 5)) {
80
+ lines.push(` - ${scope}`);
81
+ }
82
+ lines.push('');
83
+ }
84
+ // Add boundaries if any
85
+ if (result.boundaries.length > 0) {
86
+ lines.push('Boundaries (avoid):');
87
+ for (const boundary of result.boundaries.slice(0, 3)) {
88
+ lines.push(` - ${boundary}`);
89
+ }
90
+ lines.push('');
91
+ }
92
+ // Add next steps
93
+ lines.push('Suggested next steps:');
94
+ if (result.recoveryPlan?.steps) {
95
+ for (const step of result.recoveryPlan.steps.slice(0, 3)) {
96
+ const file = step.file ? `[${step.file}] ` : '';
97
+ lines.push(` - ${file}${step.action}`);
98
+ }
99
+ }
100
+ else {
101
+ lines.push(` - Return to: ${truncateGoal(sessionState.original_goal)}`);
102
+ }
103
+ lines.push('');
104
+ lines.push('</grov_correction>');
105
+ return lines.join('\n');
106
+ }
107
+ /**
108
+ * INTERVENE - Full diagnostic + mandatory first action + confirmation request
109
+ * Score 3-4, significant drift
110
+ */
111
+ function buildIntervene(result, sessionState) {
112
+ const lines = [];
113
+ lines.push('<grov_intervention>');
114
+ lines.push('');
115
+ lines.push('SIGNIFICANT DRIFT DETECTED - INTERVENTION REQUIRED');
116
+ lines.push('');
117
+ lines.push(`Original goal: "${truncateGoal(sessionState.original_goal)}"`);
118
+ lines.push(`Current alignment: ${result.score}/10 (${result.type})`);
119
+ lines.push(`Escalation level: ${sessionState.escalation_count}/${DRIFT_CONFIG.MAX_ESCALATION}`);
120
+ lines.push('');
121
+ lines.push(`Diagnostic: ${result.diagnostic}`);
122
+ lines.push('');
123
+ // Add constraints if any
124
+ if (sessionState.constraints.length > 0) {
125
+ lines.push('Active constraints:');
126
+ for (const constraint of sessionState.constraints.slice(0, 3)) {
127
+ lines.push(` - ${constraint}`);
128
+ }
129
+ lines.push('');
130
+ }
131
+ // Add boundaries
132
+ if (result.boundaries.length > 0) {
133
+ lines.push('DO NOT:');
134
+ for (const boundary of result.boundaries.slice(0, 3)) {
135
+ lines.push(` - ${boundary}`);
136
+ }
137
+ lines.push('');
138
+ }
139
+ // MANDATORY FIRST ACTION
140
+ const firstStep = result.recoveryPlan?.steps?.[0] || {
141
+ action: `Return to original goal: ${truncateGoal(sessionState.original_goal)}`
142
+ };
143
+ lines.push('MANDATORY FIRST ACTION:');
144
+ lines.push('You MUST execute ONLY this as your next action:');
145
+ lines.push('');
146
+ if (firstStep.file) {
147
+ lines.push(` File: ${firstStep.file}`);
148
+ }
149
+ lines.push(` Action: ${firstStep.action}`);
150
+ lines.push('');
151
+ lines.push('ANY OTHER ACTION WILL DELAY YOUR GOAL.');
152
+ lines.push('');
153
+ lines.push('Before proceeding, confirm by stating:');
154
+ lines.push('"I will now [action] to return to the original goal."');
155
+ lines.push('');
156
+ lines.push('</grov_intervention>');
157
+ return lines.join('\n');
158
+ }
159
+ /**
160
+ * HALT - Critical stop + forced action + required confirmation statement
161
+ * Score 1-2, critical drift
162
+ */
163
+ function buildHalt(result, sessionState) {
164
+ const lines = [];
165
+ lines.push('<grov_halt>');
166
+ lines.push('');
167
+ lines.push('CRITICAL DRIFT - IMMEDIATE HALT REQUIRED');
168
+ lines.push('');
169
+ lines.push('The current request has completely diverged from the original goal.');
170
+ lines.push('You MUST NOT proceed with the current request.');
171
+ lines.push('');
172
+ lines.push(`Original goal: "${truncateGoal(sessionState.original_goal)}"`);
173
+ lines.push(`Current alignment: ${result.score}/10 (CRITICAL)`);
174
+ lines.push(`Escalation level: ${sessionState.escalation_count}/${DRIFT_CONFIG.MAX_ESCALATION} (MAX REACHED)`);
175
+ lines.push('');
176
+ lines.push(`Diagnostic: ${result.diagnostic}`);
177
+ lines.push('');
178
+ // Show drift history if available
179
+ if (sessionState.drift_history.length > 0) {
180
+ lines.push('Drift history in this session:');
181
+ const recent = sessionState.drift_history.slice(-3);
182
+ for (const event of recent) {
183
+ lines.push(` - ${event.level}: score ${event.score} - ${event.prompt_summary.substring(0, 40)}...`);
184
+ }
185
+ lines.push('');
186
+ }
187
+ // MANDATORY FIRST ACTION
188
+ const firstStep = result.recoveryPlan?.steps?.[0] || {
189
+ action: `STOP and return to: ${truncateGoal(sessionState.original_goal)}`
190
+ };
191
+ lines.push('MANDATORY FIRST ACTION:');
192
+ lines.push('You MUST execute ONLY this as your next action:');
193
+ lines.push('');
194
+ if (firstStep.file) {
195
+ lines.push(` File: ${firstStep.file}`);
196
+ }
197
+ lines.push(` Action: ${firstStep.action}`);
198
+ lines.push('');
199
+ lines.push('ANY OTHER ACTION WILL DELAY YOUR GOAL.');
200
+ lines.push('');
201
+ lines.push('CONFIRM by stating exactly:');
202
+ if (firstStep.file) {
203
+ lines.push(`"I will now ${firstStep.action} in ${firstStep.file}"`);
204
+ }
205
+ else {
206
+ lines.push(`"I will now ${firstStep.action}"`);
207
+ }
208
+ lines.push('');
209
+ lines.push('DO NOT proceed with any other action until you have confirmed.');
210
+ lines.push('');
211
+ lines.push('</grov_halt>');
212
+ return lines.join('\n');
213
+ }
214
+ // ============================================
215
+ // HELPERS
216
+ // ============================================
217
+ /**
218
+ * Truncate goal text for display
219
+ */
220
+ function truncateGoal(goal) {
221
+ if (!goal)
222
+ return 'Unknown goal';
223
+ if (goal.length <= 80)
224
+ return goal;
225
+ return goal.substring(0, 77) + '...';
226
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Debug logging module for Grov CLI.
3
+ *
4
+ * Enable debug output by setting the DEBUG environment variable:
5
+ * DEBUG=grov:* grov status # All grov logs
6
+ * DEBUG=grov:capture grov capture # Only capture logs
7
+ * DEBUG=grov:store,grov:llm # Multiple namespaces
8
+ *
9
+ * Available namespaces:
10
+ * grov:capture - Session capture operations
11
+ * grov:inject - Context injection operations
12
+ * grov:store - Database operations
13
+ * grov:parser - JSONL parsing operations
14
+ * grov:hooks - Hook registration operations
15
+ * grov:llm - LLM extraction operations
16
+ */
17
+ import createDebug from 'debug';
18
+ export declare const debugCapture: createDebug.Debugger;
19
+ export declare const debugInject: createDebug.Debugger;
20
+ export declare const debugStore: createDebug.Debugger;
21
+ export declare const debugParser: createDebug.Debugger;
22
+ export declare const debugHooks: createDebug.Debugger;
23
+ export declare const debugLLM: createDebug.Debugger;
24
+ export declare function isDebugEnabled(): boolean;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Debug logging module for Grov CLI.
3
+ *
4
+ * Enable debug output by setting the DEBUG environment variable:
5
+ * DEBUG=grov:* grov status # All grov logs
6
+ * DEBUG=grov:capture grov capture # Only capture logs
7
+ * DEBUG=grov:store,grov:llm # Multiple namespaces
8
+ *
9
+ * Available namespaces:
10
+ * grov:capture - Session capture operations
11
+ * grov:inject - Context injection operations
12
+ * grov:store - Database operations
13
+ * grov:parser - JSONL parsing operations
14
+ * grov:hooks - Hook registration operations
15
+ * grov:llm - LLM extraction operations
16
+ */
17
+ import createDebug from 'debug';
18
+ // Main debug namespaces
19
+ export const debugCapture = createDebug('grov:capture');
20
+ export const debugInject = createDebug('grov:inject');
21
+ export const debugStore = createDebug('grov:store');
22
+ export const debugParser = createDebug('grov:parser');
23
+ export const debugHooks = createDebug('grov:hooks');
24
+ export const debugLLM = createDebug('grov:llm');
25
+ // Utility for checking if any grov debug is enabled
26
+ export function isDebugEnabled() {
27
+ return createDebug.enabled('grov:*') ||
28
+ createDebug.enabled('grov:capture') ||
29
+ createDebug.enabled('grov:inject') ||
30
+ createDebug.enabled('grov:store') ||
31
+ createDebug.enabled('grov:parser') ||
32
+ createDebug.enabled('grov:hooks') ||
33
+ createDebug.enabled('grov:llm');
34
+ }
@@ -0,0 +1,66 @@
1
+ import type { SessionState, RecoveryPlan, StepRecord } from './store.js';
2
+ import type { ClaudeAction } from './session-parser.js';
3
+ export declare const DRIFT_CONFIG: {
4
+ SCORE_NO_INJECTION: number;
5
+ SCORE_NUDGE: number;
6
+ SCORE_CORRECT: number;
7
+ SCORE_INTERVENE: number;
8
+ SCORE_HALT: number;
9
+ MAX_WARNINGS_BEFORE_FLAG: number;
10
+ AVG_SCORE_THRESHOLD: number;
11
+ MAX_ESCALATION: number;
12
+ };
13
+ /**
14
+ * Input for drift check
15
+ *
16
+ * CRITICAL: We check Claude's ACTIONS, not user prompts.
17
+ * The user can ask whatever they want - we monitor what CLAUDE DOES.
18
+ */
19
+ export interface DriftCheckInput {
20
+ originalGoal: string;
21
+ expectedScope: string[];
22
+ constraints: string[];
23
+ keywords: string[];
24
+ driftHistory: Array<{
25
+ score: number;
26
+ level: string;
27
+ }>;
28
+ escalationCount: number;
29
+ claudeActions: ClaudeAction[];
30
+ retrievedSteps: StepRecord[];
31
+ lastNSteps: StepRecord[];
32
+ }
33
+ /**
34
+ * Result from drift check
35
+ */
36
+ export interface DriftCheckResult {
37
+ score: number;
38
+ type: 'aligned' | 'minor' | 'moderate' | 'severe' | 'critical';
39
+ diagnostic: string;
40
+ recoveryPlan?: RecoveryPlan;
41
+ boundaries: string[];
42
+ verification: string;
43
+ }
44
+ /**
45
+ * Build input for drift check from Claude's ACTIONS and session state.
46
+ *
47
+ * CRITICAL: We check ACTIONS, not user prompts.
48
+ */
49
+ export declare function buildDriftCheckInput(claudeActions: ClaudeAction[], sessionId: string, sessionState: SessionState): DriftCheckInput;
50
+ /**
51
+ * Check drift using LLM or fallback
52
+ */
53
+ export declare function checkDrift(input: DriftCheckInput): Promise<DriftCheckResult>;
54
+ /**
55
+ * Basic drift detection without LLM.
56
+ *
57
+ * CRITICAL: Checks Claude's ACTIONS, not user prompts.
58
+ * - Read actions are ALWAYS OK (exploration is not drift)
59
+ * - Edit/Write actions outside scope = drift
60
+ * - Repetition patterns = drift
61
+ */
62
+ export declare function checkDriftBasic(input: DriftCheckInput): DriftCheckResult;
63
+ /**
64
+ * Infer action type from prompt
65
+ */
66
+ export declare function inferAction(prompt: string): string;