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.
- package/LICENSE +190 -0
- package/README.md +211 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +106 -0
- package/dist/commands/capture.d.ts +6 -0
- package/dist/commands/capture.js +324 -0
- package/dist/commands/drift-test.d.ts +7 -0
- package/dist/commands/drift-test.js +177 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +27 -0
- package/dist/commands/inject.d.ts +5 -0
- package/dist/commands/inject.js +88 -0
- package/dist/commands/prompt-inject.d.ts +4 -0
- package/dist/commands/prompt-inject.js +451 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/unregister.d.ts +1 -0
- package/dist/commands/unregister.js +22 -0
- package/dist/lib/anchor-extractor.d.ts +30 -0
- package/dist/lib/anchor-extractor.js +296 -0
- package/dist/lib/correction-builder.d.ts +10 -0
- package/dist/lib/correction-builder.js +226 -0
- package/dist/lib/debug.d.ts +24 -0
- package/dist/lib/debug.js +34 -0
- package/dist/lib/drift-checker.d.ts +66 -0
- package/dist/lib/drift-checker.js +341 -0
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +258 -0
- package/dist/lib/jsonl-parser.d.ts +87 -0
- package/dist/lib/jsonl-parser.js +281 -0
- package/dist/lib/llm-extractor.d.ts +50 -0
- package/dist/lib/llm-extractor.js +408 -0
- package/dist/lib/session-parser.d.ts +44 -0
- package/dist/lib/session-parser.js +256 -0
- package/dist/lib/store.d.ts +248 -0
- package/dist/lib/store.js +793 -0
- package/dist/lib/utils.d.ts +31 -0
- package/dist/lib/utils.js +76 -0
- 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;
|