@haystackeditor/cli 0.7.2 → 0.8.1
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 +59 -12
- package/dist/assets/hooks/agent-context/detect.ts +136 -0
- package/dist/assets/hooks/agent-context/format.ts +99 -0
- package/dist/assets/hooks/agent-context/index.ts +39 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
- package/dist/assets/hooks/agent-context/types.ts +58 -0
- package/dist/assets/hooks/llm-rules-template.md +35 -0
- package/dist/assets/hooks/package.json +11 -0
- package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
- package/dist/assets/hooks/scripts/post-commit.sh +4 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
- package/dist/assets/hooks/scripts/pre-push.sh +5 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
- package/dist/assets/hooks/truncation-checker/index.ts +595 -0
- package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +89 -0
- package/dist/commands/hooks.d.ts +17 -0
- package/dist/commands/hooks.js +269 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +20 -239
- package/dist/commands/secrets.d.ts +15 -0
- package/dist/commands/secrets.js +83 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +215 -0
- package/dist/index.js +107 -7
- package/dist/types.d.ts +32 -8
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +226 -0
- package/dist/utils/skill.d.ts +1 -1
- package/dist/utils/skill.js +481 -13
- package/package.json +2 -2
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncation Pattern Checker
|
|
3
|
+
*
|
|
4
|
+
* Scans staged git diff for patterns that violate the "No Truncation Without Permission" rule.
|
|
5
|
+
* See LLM_RULES.md for the full policy.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { analyzeFiles, formatASTViolations, type ASTViolation } from './ast-analyzer.js';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// CONFIGURATION
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/** Paths that should be checked for truncation violations */
|
|
17
|
+
const CHECKED_PATH_PATTERNS = [
|
|
18
|
+
/^agent\//,
|
|
19
|
+
/^analysis\//,
|
|
20
|
+
/\/prompts?\//i,
|
|
21
|
+
/tool/i,
|
|
22
|
+
/llm/i,
|
|
23
|
+
/agent/i,
|
|
24
|
+
/pipeline/i,
|
|
25
|
+
/context/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Paths that should be excluded from checks */
|
|
29
|
+
const EXCLUDED_PATH_PATTERNS = [
|
|
30
|
+
/node_modules/,
|
|
31
|
+
/\.test\./,
|
|
32
|
+
/\.spec\./,
|
|
33
|
+
/__tests__/,
|
|
34
|
+
/\/test\//,
|
|
35
|
+
/\/tests\//,
|
|
36
|
+
/^hooks\//, // Don't check our own hook code
|
|
37
|
+
/\.md$/,
|
|
38
|
+
/\.json$/,
|
|
39
|
+
/\.yaml$/,
|
|
40
|
+
/\.yml$/,
|
|
41
|
+
/\.svg$/,
|
|
42
|
+
/\.png$/,
|
|
43
|
+
/\.jpg$/,
|
|
44
|
+
/\.gif$/,
|
|
45
|
+
/\.ico$/,
|
|
46
|
+
/\.lock$/,
|
|
47
|
+
/package-lock/,
|
|
48
|
+
/pnpm-lock/,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// PATTERN DEFINITIONS
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
interface TruncationPattern {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
description: string;
|
|
59
|
+
patterns: RegExp[];
|
|
60
|
+
severity: 'error' | 'warning';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const TRUNCATION_PATTERNS: TruncationPattern[] = [
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
// Category 1: Direct slice/substring with numeric limits
|
|
66
|
+
// -------------------------------------------------------------------------
|
|
67
|
+
{
|
|
68
|
+
id: 'slice-numeric',
|
|
69
|
+
name: 'Slice with numeric limit',
|
|
70
|
+
description: 'Direct .slice(0, N) or .substring(0, N) with hardcoded limit',
|
|
71
|
+
patterns: [
|
|
72
|
+
/\.slice\s*\(\s*0\s*,\s*\d+\s*\)/,
|
|
73
|
+
/\.substring\s*\(\s*0\s*,\s*\d+\s*\)/,
|
|
74
|
+
/\.substr\s*\(\s*0\s*,\s*\d+\s*\)/,
|
|
75
|
+
],
|
|
76
|
+
severity: 'error',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'slice-variable',
|
|
80
|
+
name: 'Slice with limit variable',
|
|
81
|
+
description: '.slice(0, limit) or .slice(0, max...) with variable limit',
|
|
82
|
+
patterns: [
|
|
83
|
+
/\.slice\s*\(\s*0\s*,\s*(?:max|limit|MAX|LIMIT|cutoff|truncate)/i,
|
|
84
|
+
/\.substring\s*\(\s*0\s*,\s*(?:max|limit|MAX|LIMIT|cutoff|truncate)/i,
|
|
85
|
+
/\.slice\s*\(\s*0\s*,\s*[A-Z_]+(?:LENGTH|LIMIT|SIZE|COUNT|ITEMS|LINES|CHARS|TOKENS)\s*\)/,
|
|
86
|
+
],
|
|
87
|
+
severity: 'error',
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
// Category 2: Ellipsis additions indicating truncation
|
|
92
|
+
// -------------------------------------------------------------------------
|
|
93
|
+
{
|
|
94
|
+
id: 'ellipsis-concat',
|
|
95
|
+
name: 'Ellipsis concatenation',
|
|
96
|
+
description: 'Adding "..." or ellipsis character to indicate truncation',
|
|
97
|
+
patterns: [
|
|
98
|
+
/\+\s*['"`]\.\.\.['"`]/,
|
|
99
|
+
/\+\s*['"`]…['"`]/, // Unicode ellipsis
|
|
100
|
+
/\+\s*['"`]\[\.\.\.?\]]['"`]/,
|
|
101
|
+
/\+\s*['"`]\s*\.\.\.\s*\(truncated\)['"`]/i,
|
|
102
|
+
/\+\s*['"`]\[truncated\]['"`]/i,
|
|
103
|
+
/\+\s*['"`]\(truncated\)['"`]/i,
|
|
104
|
+
/\+\s*['"`]\[omitted\]['"`]/i,
|
|
105
|
+
/\+\s*['"`]\(omitted\)['"`]/i,
|
|
106
|
+
/['"`]\.\.\.['"`]\s*\+/, // Prefix ellipsis
|
|
107
|
+
],
|
|
108
|
+
severity: 'error',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'ellipsis-template',
|
|
112
|
+
name: 'Ellipsis in template literal',
|
|
113
|
+
description: 'Template literal with ellipsis pattern',
|
|
114
|
+
patterns: [
|
|
115
|
+
/`[^`]*\.\.\.[^`]*\$\{/,
|
|
116
|
+
/`[^`]*\$\{[^}]+\}[^`]*\.\.\.`/,
|
|
117
|
+
/`\.\.\./,
|
|
118
|
+
],
|
|
119
|
+
severity: 'warning', // Lower severity - could be legitimate UI
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// Category 3: "N more" patterns
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
{
|
|
126
|
+
id: 'n-more-pattern',
|
|
127
|
+
name: '"N more" truncation indicator',
|
|
128
|
+
description: 'Patterns like "and X more", "+ N others"',
|
|
129
|
+
patterns: [
|
|
130
|
+
/\$\{[^}]+\}\s*more/i,
|
|
131
|
+
/`[^`]*and\s+\$\{[^}]+\}\s+more[^`]*`/i,
|
|
132
|
+
/`[^`]*\+\s*\$\{[^}]+\}\s*(?:more|others|items|remaining)[^`]*`/i,
|
|
133
|
+
/['"`]\s*\.\.\.\s*and\s+['"`]\s*\+/i,
|
|
134
|
+
/\[\s*\+\s*\$\{[^}]+\}\s*\]/,
|
|
135
|
+
/`[^`]*\$\{[^}]+\}\s+additional[^`]*`/i,
|
|
136
|
+
/`[^`]*\$\{[^}]+\}\s+remaining[^`]*`/i,
|
|
137
|
+
],
|
|
138
|
+
severity: 'error',
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// Category 4: "First N" / "Showing N" patterns
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
{
|
|
145
|
+
id: 'showing-first-n',
|
|
146
|
+
name: '"Showing first N" pattern',
|
|
147
|
+
description: 'Patterns indicating partial display',
|
|
148
|
+
patterns: [
|
|
149
|
+
/['"`](?:showing|displaying)\s+(?:first|only)\s+\d+/i,
|
|
150
|
+
/['"`]first\s+\d+\s+(?:results?|items?|lines?|entries)/i,
|
|
151
|
+
/['"`]limited\s+to\s+\d+/i,
|
|
152
|
+
/['"`]only\s+showing\s+\d+/i,
|
|
153
|
+
/['"`]top\s+\d+\s+(?:results?|items?)/i,
|
|
154
|
+
/['"`]truncated\s+to\s+\d+/i,
|
|
155
|
+
],
|
|
156
|
+
severity: 'error',
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// -------------------------------------------------------------------------
|
|
160
|
+
// Category 5: Truncation functions
|
|
161
|
+
// -------------------------------------------------------------------------
|
|
162
|
+
{
|
|
163
|
+
id: 'truncate-function',
|
|
164
|
+
name: 'Truncation function call',
|
|
165
|
+
description: 'Calling truncate(), shortenText(), etc.',
|
|
166
|
+
patterns: [
|
|
167
|
+
/(?<!function\s)(?<!\.)\btruncate\s*\(/i,
|
|
168
|
+
/(?<!function\s)truncateText\s*\(/i,
|
|
169
|
+
/(?<!function\s)truncateString\s*\(/i,
|
|
170
|
+
/(?<!function\s)shortenText\s*\(/i,
|
|
171
|
+
/(?<!function\s)limitLength\s*\(/i,
|
|
172
|
+
/(?<!function\s)ellipsize\s*\(/i,
|
|
173
|
+
/(?<!function\s)abbreviate\s*\(/i,
|
|
174
|
+
/_\.truncate\s*\(/,
|
|
175
|
+
/_\.take\s*\(/,
|
|
176
|
+
/lodash.*\.truncate\s*\(/,
|
|
177
|
+
],
|
|
178
|
+
severity: 'error',
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
// Category 6: Conditional length checks with truncation
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
{
|
|
185
|
+
id: 'conditional-truncation',
|
|
186
|
+
name: 'Conditional length check',
|
|
187
|
+
description: 'if (x.length > N) patterns suggesting truncation',
|
|
188
|
+
patterns: [
|
|
189
|
+
/if\s*\([^)]*\.length\s*>\s*\d+[^)]*\)\s*\{[^}]*\.slice/,
|
|
190
|
+
/if\s*\([^)]*\.length\s*>\s*(?:max|limit|MAX|LIMIT)/i,
|
|
191
|
+
/\.length\s*>\s*\d+\s*\?\s*[^:]+\.slice\s*\(/,
|
|
192
|
+
/\.length\s*>\s*(?:max|limit|MAX|LIMIT)[^?]*\?[^:]*\.slice/i,
|
|
193
|
+
],
|
|
194
|
+
severity: 'error',
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// -------------------------------------------------------------------------
|
|
198
|
+
// Category 7: Content omission markers
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
{
|
|
201
|
+
id: 'omission-marker',
|
|
202
|
+
name: 'Content omission marker',
|
|
203
|
+
description: 'Strings indicating content was omitted',
|
|
204
|
+
patterns: [
|
|
205
|
+
/['"`]\[content\s+omitted\]['"`]/i,
|
|
206
|
+
/['"`]\[\.\.\.\s*snip\s*\.\.\.\]['"`]/i,
|
|
207
|
+
/['"`]<!--\s*truncated\s*-->['"`]/i,
|
|
208
|
+
/['"`]\[showing\s+\d+\s+of\s+\d+\]['"`]/i,
|
|
209
|
+
/['"`]\[\d+\s+items?\s+hidden\]['"`]/i,
|
|
210
|
+
/['"`]\[rest\s+omitted\]['"`]/i,
|
|
211
|
+
/['"`]output\s+truncated['"`]/i,
|
|
212
|
+
/['"`]results?\s+truncated['"`]/i,
|
|
213
|
+
],
|
|
214
|
+
severity: 'error',
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// -------------------------------------------------------------------------
|
|
218
|
+
// Category 8: Array truncation
|
|
219
|
+
// -------------------------------------------------------------------------
|
|
220
|
+
{
|
|
221
|
+
id: 'array-truncation',
|
|
222
|
+
name: 'Array truncation',
|
|
223
|
+
description: 'Truncating arrays via length assignment or splice',
|
|
224
|
+
patterns: [
|
|
225
|
+
/\.length\s*=\s*\d+/, // array.length = N (truncates)
|
|
226
|
+
/\.length\s*=\s*(?:max|limit|MAX|LIMIT)/i,
|
|
227
|
+
/\.splice\s*\(\s*\d+\s*\)/, // splice(N) removes from index N
|
|
228
|
+
/\.splice\s*\(\s*(?:max|limit|MAX|LIMIT)/i,
|
|
229
|
+
],
|
|
230
|
+
severity: 'warning',
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// -------------------------------------------------------------------------
|
|
234
|
+
// Category 9: Take/head operations (lodash style)
|
|
235
|
+
// -------------------------------------------------------------------------
|
|
236
|
+
{
|
|
237
|
+
id: 'take-head',
|
|
238
|
+
name: 'Take/head operations',
|
|
239
|
+
description: 'Lodash-style take() or head() limiting',
|
|
240
|
+
patterns: [
|
|
241
|
+
/\.take\s*\(\s*\d+\s*\)/,
|
|
242
|
+
/\.head\s*\(\s*\d+\s*\)/,
|
|
243
|
+
/\.first\s*\(\s*\d+\s*\)/,
|
|
244
|
+
/\.limit\s*\(\s*\d+\s*\)/,
|
|
245
|
+
],
|
|
246
|
+
severity: 'warning',
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
// -------------------------------------------------------------------------
|
|
250
|
+
// Category 10: Max/limit constants being defined (context: likely used for truncation)
|
|
251
|
+
// -------------------------------------------------------------------------
|
|
252
|
+
{
|
|
253
|
+
id: 'limit-constant',
|
|
254
|
+
name: 'Limit constant definition',
|
|
255
|
+
description: 'Defining MAX_* or *_LIMIT constants',
|
|
256
|
+
patterns: [
|
|
257
|
+
/(?:const|let|var)\s+MAX_(?:LENGTH|ITEMS|LINES|TOKENS|CHARS|RESULTS|OUTPUT|CONTENT)\s*=/i,
|
|
258
|
+
/(?:const|let|var)\s+(?:LENGTH|ITEMS|LINES|TOKENS|CHARS|RESULTS|OUTPUT|CONTENT)_(?:LIMIT|MAX)\s*=/i,
|
|
259
|
+
/(?:const|let|var)\s+(?:max|limit)(?:Length|Items|Lines|Tokens|Chars|Results|Output|Content)\s*=/,
|
|
260
|
+
/(?:const|let|var)\s+TRUNCATE_(?:AT|AFTER|LENGTH)\s*=/i,
|
|
261
|
+
/(?:const|let|var)\s+head_limit\s*=/i,
|
|
262
|
+
],
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// -------------------------------------------------------------------------
|
|
267
|
+
// Category 11: Output/result slicing (common in agent code)
|
|
268
|
+
// -------------------------------------------------------------------------
|
|
269
|
+
{
|
|
270
|
+
id: 'output-slicing',
|
|
271
|
+
name: 'Output/result slicing',
|
|
272
|
+
description: 'Slicing output, result, content, or text variables',
|
|
273
|
+
patterns: [
|
|
274
|
+
/(?:output|result|content|text|response|data|items|lines|entries)\s*(?:=|\.)\s*[^;]*\.slice\s*\(\s*0\s*,/i,
|
|
275
|
+
/(?:output|result|content|text|response|data)\s*=\s*[^;]*\.substring\s*\(\s*0\s*,/i,
|
|
276
|
+
],
|
|
277
|
+
severity: 'error',
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// VIOLATION DETECTION
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
export interface Violation {
|
|
286
|
+
file: string;
|
|
287
|
+
line: number;
|
|
288
|
+
column: number;
|
|
289
|
+
content: string;
|
|
290
|
+
pattern: TruncationPattern;
|
|
291
|
+
matchedText: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface DiffHunk {
|
|
295
|
+
file: string;
|
|
296
|
+
lines: Array<{
|
|
297
|
+
lineNumber: number;
|
|
298
|
+
content: string;
|
|
299
|
+
type: 'add' | 'remove' | 'context';
|
|
300
|
+
}>;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function parseDiff(diffOutput: string): DiffHunk[] {
|
|
304
|
+
const hunks: DiffHunk[] = [];
|
|
305
|
+
let currentFile = '';
|
|
306
|
+
let currentHunk: DiffHunk | null = null;
|
|
307
|
+
let lineNumber = 0;
|
|
308
|
+
|
|
309
|
+
for (const line of diffOutput.split('\n')) {
|
|
310
|
+
// New file
|
|
311
|
+
if (line.startsWith('+++ b/')) {
|
|
312
|
+
currentFile = line.slice(6);
|
|
313
|
+
currentHunk = { file: currentFile, lines: [] };
|
|
314
|
+
hunks.push(currentHunk);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Hunk header: @@ -old,count +new,count @@
|
|
319
|
+
const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
|
|
320
|
+
if (hunkMatch) {
|
|
321
|
+
lineNumber = parseInt(hunkMatch[1], 10) - 1; // Will be incremented on first line
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Only process if we're in a file
|
|
326
|
+
if (!currentHunk) continue;
|
|
327
|
+
|
|
328
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
329
|
+
lineNumber++;
|
|
330
|
+
currentHunk.lines.push({
|
|
331
|
+
lineNumber,
|
|
332
|
+
content: line.slice(1), // Remove the '+' prefix
|
|
333
|
+
type: 'add',
|
|
334
|
+
});
|
|
335
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
336
|
+
// Removed lines don't increment lineNumber
|
|
337
|
+
// We don't check removed lines
|
|
338
|
+
} else if (line.startsWith(' ')) {
|
|
339
|
+
lineNumber++;
|
|
340
|
+
// Context line, don't check
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return hunks;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function shouldCheckFile(filePath: string): boolean {
|
|
348
|
+
// Check exclusions first
|
|
349
|
+
for (const pattern of EXCLUDED_PATH_PATTERNS) {
|
|
350
|
+
if (pattern.test(filePath)) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check if it matches any included path
|
|
356
|
+
for (const pattern of CHECKED_PATH_PATTERNS) {
|
|
357
|
+
if (pattern.test(filePath)) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function findViolations(hunks: DiffHunk[]): Violation[] {
|
|
366
|
+
const violations: Violation[] = [];
|
|
367
|
+
|
|
368
|
+
for (const hunk of hunks) {
|
|
369
|
+
if (!shouldCheckFile(hunk.file)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const line of hunk.lines) {
|
|
374
|
+
if (line.type !== 'add') continue;
|
|
375
|
+
|
|
376
|
+
// Skip comments (basic heuristic)
|
|
377
|
+
const trimmed = line.content.trim();
|
|
378
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
|
|
379
|
+
// But still check if it's a suspicious comment
|
|
380
|
+
if (!/intentional|pagination|user.?control|by.?design|expected/i.test(line.content)) {
|
|
381
|
+
// Check for omission markers in comments too
|
|
382
|
+
for (const pattern of TRUNCATION_PATTERNS.filter(p => p.id === 'omission-marker')) {
|
|
383
|
+
for (const regex of pattern.patterns) {
|
|
384
|
+
const match = regex.exec(line.content);
|
|
385
|
+
if (match) {
|
|
386
|
+
violations.push({
|
|
387
|
+
file: hunk.file,
|
|
388
|
+
line: line.lineNumber,
|
|
389
|
+
column: match.index + 1,
|
|
390
|
+
content: line.content,
|
|
391
|
+
pattern,
|
|
392
|
+
matchedText: match[0],
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check each pattern
|
|
402
|
+
for (const pattern of TRUNCATION_PATTERNS) {
|
|
403
|
+
for (const regex of pattern.patterns) {
|
|
404
|
+
const match = regex.exec(line.content);
|
|
405
|
+
if (match) {
|
|
406
|
+
violations.push({
|
|
407
|
+
file: hunk.file,
|
|
408
|
+
line: line.lineNumber,
|
|
409
|
+
column: match.index + 1,
|
|
410
|
+
content: line.content,
|
|
411
|
+
pattern,
|
|
412
|
+
matchedText: match[0],
|
|
413
|
+
});
|
|
414
|
+
break; // One match per pattern per line is enough
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return violations;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// OUTPUT FORMATTING
|
|
426
|
+
// ============================================================================
|
|
427
|
+
|
|
428
|
+
function formatViolations(violations: Violation[]): string {
|
|
429
|
+
if (violations.length === 0) {
|
|
430
|
+
return '';
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const lines: string[] = [];
|
|
434
|
+
lines.push('');
|
|
435
|
+
lines.push('========================================');
|
|
436
|
+
lines.push(' TRUNCATION VIOLATIONS DETECTED');
|
|
437
|
+
lines.push('========================================');
|
|
438
|
+
lines.push('');
|
|
439
|
+
lines.push('The following changes appear to violate the "No Truncation Without Permission" rule.');
|
|
440
|
+
lines.push('See LLM_RULES.md for the full policy.');
|
|
441
|
+
lines.push('');
|
|
442
|
+
|
|
443
|
+
// Group by file
|
|
444
|
+
const byFile = new Map<string, Violation[]>();
|
|
445
|
+
for (const v of violations) {
|
|
446
|
+
const existing = byFile.get(v.file) || [];
|
|
447
|
+
existing.push(v);
|
|
448
|
+
byFile.set(v.file, existing);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (const [file, fileViolations] of byFile) {
|
|
452
|
+
lines.push(`📄 ${file}`);
|
|
453
|
+
for (const v of fileViolations) {
|
|
454
|
+
const severity = v.pattern.severity === 'error' ? '❌' : '⚠️';
|
|
455
|
+
lines.push(` ${severity} Line ${v.line}: ${v.pattern.name}`);
|
|
456
|
+
lines.push(` Pattern: ${v.pattern.description}`);
|
|
457
|
+
lines.push(` Matched: ${v.matchedText}`);
|
|
458
|
+
lines.push(` Code: ${v.content.trim().slice(0, 100)}${v.content.length > 100 ? '...' : ''}`);
|
|
459
|
+
lines.push('');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
lines.push('----------------------------------------');
|
|
464
|
+
lines.push('');
|
|
465
|
+
lines.push('If this truncation is intentional and user-approved:');
|
|
466
|
+
lines.push(' 1. Add a comment explaining why truncation is needed');
|
|
467
|
+
lines.push(' 2. Include "intentional" or "user-approved" in the comment');
|
|
468
|
+
lines.push(' 3. Document what content is being truncated and how much');
|
|
469
|
+
lines.push('');
|
|
470
|
+
lines.push('If this is pagination or user-controlled:');
|
|
471
|
+
lines.push(' 1. Ensure the user can access the full content');
|
|
472
|
+
lines.push(' 2. Add a comment with "pagination" or "user-control"');
|
|
473
|
+
lines.push('');
|
|
474
|
+
lines.push('========================================');
|
|
475
|
+
|
|
476
|
+
return lines.join('\n');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ============================================================================
|
|
480
|
+
// MAIN
|
|
481
|
+
// ============================================================================
|
|
482
|
+
|
|
483
|
+
export interface CheckResult {
|
|
484
|
+
violations: Violation[];
|
|
485
|
+
astViolations: ASTViolation[];
|
|
486
|
+
hasErrors: boolean;
|
|
487
|
+
hasWarnings: boolean;
|
|
488
|
+
output: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function getStagedFiles(): string[] {
|
|
492
|
+
try {
|
|
493
|
+
const output = execSync('git diff --cached --name-only', {
|
|
494
|
+
encoding: 'utf-8',
|
|
495
|
+
});
|
|
496
|
+
return output
|
|
497
|
+
.trim()
|
|
498
|
+
.split('\n')
|
|
499
|
+
.filter((f) => f && shouldCheckFile(f));
|
|
500
|
+
} catch {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function getRepoRoot(): string {
|
|
506
|
+
try {
|
|
507
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
508
|
+
encoding: 'utf-8',
|
|
509
|
+
}).trim();
|
|
510
|
+
} catch {
|
|
511
|
+
return process.cwd();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function checkStagedChanges(): CheckResult {
|
|
516
|
+
const startTime = Date.now();
|
|
517
|
+
|
|
518
|
+
let diffOutput: string;
|
|
519
|
+
try {
|
|
520
|
+
diffOutput = execSync('git diff --cached --unified=0', {
|
|
521
|
+
encoding: 'utf-8',
|
|
522
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
523
|
+
});
|
|
524
|
+
} catch {
|
|
525
|
+
return {
|
|
526
|
+
violations: [],
|
|
527
|
+
astViolations: [],
|
|
528
|
+
hasErrors: false,
|
|
529
|
+
hasWarnings: false,
|
|
530
|
+
output: '',
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!diffOutput.trim()) {
|
|
535
|
+
return {
|
|
536
|
+
violations: [],
|
|
537
|
+
astViolations: [],
|
|
538
|
+
hasErrors: false,
|
|
539
|
+
hasWarnings: false,
|
|
540
|
+
output: '',
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Regex-based detection (fast, catches obvious patterns)
|
|
545
|
+
const hunks = parseDiff(diffOutput);
|
|
546
|
+
const violations = findViolations(hunks);
|
|
547
|
+
|
|
548
|
+
// AST-based detection (semantic analysis)
|
|
549
|
+
const repoRoot = getRepoRoot();
|
|
550
|
+
const stagedFiles = getStagedFiles().map((f) => join(repoRoot, f));
|
|
551
|
+
const astViolations = analyzeFiles(stagedFiles);
|
|
552
|
+
|
|
553
|
+
const regexTime = Date.now() - startTime;
|
|
554
|
+
|
|
555
|
+
const hasErrors =
|
|
556
|
+
violations.some((v) => v.pattern.severity === 'error') ||
|
|
557
|
+
astViolations.some((v) => v.severity === 'error');
|
|
558
|
+
const hasWarnings =
|
|
559
|
+
violations.some((v) => v.pattern.severity === 'warning') ||
|
|
560
|
+
astViolations.some((v) => v.severity === 'warning');
|
|
561
|
+
|
|
562
|
+
// Combine output
|
|
563
|
+
let output = formatViolations(violations);
|
|
564
|
+
const astOutput = formatASTViolations(astViolations);
|
|
565
|
+
if (astOutput) {
|
|
566
|
+
output += '\n' + astOutput;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Add timing info in verbose mode
|
|
570
|
+
if (process.env.TRUNCATION_CHECK_VERBOSE) {
|
|
571
|
+
output += `\n[Timing: regex=${regexTime}ms, total=${Date.now() - startTime}ms, files=${stagedFiles.length}]`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
violations,
|
|
576
|
+
astViolations,
|
|
577
|
+
hasErrors,
|
|
578
|
+
hasWarnings,
|
|
579
|
+
output,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// CLI entry point
|
|
584
|
+
if (process.argv[1]?.endsWith('index.ts') || process.argv[1]?.endsWith('index.js')) {
|
|
585
|
+
const result = checkStagedChanges();
|
|
586
|
+
if (result.output) {
|
|
587
|
+
console.error(result.output);
|
|
588
|
+
}
|
|
589
|
+
// Exit with error if any errors found (regex or AST)
|
|
590
|
+
if (result.hasErrors) {
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export { type ASTViolation } from './ast-analyzer.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["./**/*.ts"]
|
|
13
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config commands - manage user preferences stored on Haystack Platform
|
|
3
3
|
*/
|
|
4
|
+
type AgenticTool = 'opencode' | 'claude-code' | 'codex';
|
|
4
5
|
/**
|
|
5
6
|
* Get current sandbox status
|
|
6
7
|
*/
|
|
@@ -17,3 +18,16 @@ export declare function disableSandbox(): Promise<void>;
|
|
|
17
18
|
* Handle sandbox subcommand
|
|
18
19
|
*/
|
|
19
20
|
export declare function handleSandbox(action?: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Get current agentic tool setting
|
|
23
|
+
*/
|
|
24
|
+
export declare function getAgenticToolStatus(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Set agentic tool preference
|
|
27
|
+
*/
|
|
28
|
+
export declare function setAgenticTool(tool: AgenticTool): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Handle agentic-tool subcommand
|
|
31
|
+
*/
|
|
32
|
+
export declare function handleAgenticTool(tool?: string): Promise<void>;
|
|
33
|
+
export {};
|