@haystackeditor/cli 0.8.0 → 0.9.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 (76) hide show
  1. package/README.md +105 -12
  2. package/dist/assets/hooks/agent-context/detect.ts +136 -0
  3. package/dist/assets/hooks/agent-context/format.ts +99 -0
  4. package/dist/assets/hooks/agent-context/index.ts +39 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
  6. package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
  7. package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
  8. package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
  9. package/dist/assets/hooks/agent-context/types.ts +58 -0
  10. package/dist/assets/hooks/llm-rules-template.md +56 -0
  11. package/dist/assets/hooks/package.json +11 -0
  12. package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
  13. package/dist/assets/hooks/scripts/post-commit.sh +4 -0
  14. package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
  15. package/dist/assets/hooks/scripts/pre-push.sh +25 -0
  16. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  17. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
  18. package/dist/assets/hooks/truncation-checker/index.ts +595 -0
  19. package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
  20. package/dist/assets/skills/prepare-haystack.md +323 -0
  21. package/dist/assets/skills/secrets.md +164 -0
  22. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  23. package/dist/assets/skills/setup-haystack.md +639 -0
  24. package/dist/assets/skills/submit.md +154 -0
  25. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  26. package/dist/assets/templates/haystack.yml +193 -0
  27. package/dist/commands/check-pending.d.ts +19 -0
  28. package/dist/commands/check-pending.js +217 -0
  29. package/dist/commands/config.d.ts +18 -12
  30. package/dist/commands/config.js +327 -52
  31. package/dist/commands/hooks.d.ts +17 -0
  32. package/dist/commands/hooks.js +269 -0
  33. package/dist/commands/install-session-hooks.d.ts +16 -0
  34. package/dist/commands/install-session-hooks.js +302 -0
  35. package/dist/commands/login.js +1 -1
  36. package/dist/commands/policy.d.ts +31 -0
  37. package/dist/commands/policy.js +365 -0
  38. package/dist/commands/skills.d.ts +8 -0
  39. package/dist/commands/skills.js +80 -0
  40. package/dist/commands/submit.d.ts +22 -0
  41. package/dist/commands/submit.js +428 -0
  42. package/dist/commands/triage.d.ts +16 -0
  43. package/dist/commands/triage.js +354 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +317 -2
  46. package/dist/tools/detect.d.ts +50 -0
  47. package/dist/tools/detect.js +853 -0
  48. package/dist/tools/fixtures.d.ts +38 -0
  49. package/dist/tools/fixtures.js +199 -0
  50. package/dist/tools/setup.d.ts +43 -0
  51. package/dist/tools/setup.js +597 -0
  52. package/dist/triage/prompts.d.ts +31 -0
  53. package/dist/triage/prompts.js +266 -0
  54. package/dist/triage/runner.d.ts +21 -0
  55. package/dist/triage/runner.js +325 -0
  56. package/dist/triage/traces.d.ts +20 -0
  57. package/dist/triage/traces.js +305 -0
  58. package/dist/triage/types.d.ts +47 -0
  59. package/dist/triage/types.js +7 -0
  60. package/dist/types.d.ts +1387 -191
  61. package/dist/types.js +254 -2
  62. package/dist/utils/analysis-api.d.ts +108 -0
  63. package/dist/utils/analysis-api.js +194 -0
  64. package/dist/utils/config.js +1 -1
  65. package/dist/utils/git.d.ts +80 -0
  66. package/dist/utils/git.js +302 -0
  67. package/dist/utils/github-api.d.ts +83 -0
  68. package/dist/utils/github-api.js +266 -0
  69. package/dist/utils/hooks.d.ts +26 -0
  70. package/dist/utils/hooks.js +226 -0
  71. package/dist/utils/pending-state.d.ts +38 -0
  72. package/dist/utils/pending-state.js +86 -0
  73. package/dist/utils/secrets.js +3 -3
  74. package/dist/utils/skill.d.ts +1 -1
  75. package/dist/utils/skill.js +658 -1
  76. package/package.json +5 -3
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Local Entire CLI trace discovery for intent drift analysis.
3
+ *
4
+ * Finds .entire/metadata/ session files whose modified files
5
+ * overlap with the current diff's changed files.
6
+ */
7
+ import { execSync } from 'child_process';
8
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ /**
11
+ * Get the list of files changed in the current diff vs base branch.
12
+ */
13
+ function getChangedFiles(gitRoot, baseBranch) {
14
+ try {
15
+ const output = execSync(`git diff ${baseBranch}...HEAD --name-only`, {
16
+ encoding: 'utf-8',
17
+ cwd: gitRoot,
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ }).trim();
20
+ if (!output)
21
+ return [];
22
+ return output.split('\n').filter(Boolean);
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ /**
29
+ * Extract file paths mentioned in a JSONL trace file.
30
+ *
31
+ * Looks for file paths in tool_use content blocks (Write, Edit, Read operations)
32
+ * by scanning for common path patterns in the JSONL entries.
33
+ */
34
+ function extractFilesFromTrace(tracePath) {
35
+ const files = new Set();
36
+ try {
37
+ const content = readFileSync(tracePath, 'utf-8');
38
+ const lines = content.split('\n').filter(Boolean);
39
+ for (const line of lines) {
40
+ try {
41
+ const entry = JSON.parse(line);
42
+ // Look for file paths in tool use inputs
43
+ // Claude Code format: { type: "tool_use", name: "Write"|"Edit"|"Read", input: { file_path: "..." } }
44
+ if (entry?.type === 'tool_use' || entry?.message?.content) {
45
+ const text = typeof entry === 'string' ? entry : JSON.stringify(entry);
46
+ // Match file_path values
47
+ const filePathMatches = text.match(/"file_path"\s*:\s*"([^"]+)"/g);
48
+ if (filePathMatches) {
49
+ for (const match of filePathMatches) {
50
+ const pathMatch = match.match(/"file_path"\s*:\s*"([^"]+)"/);
51
+ if (pathMatch?.[1]) {
52
+ // Normalize: strip leading ./ and absolute path prefixes
53
+ let filePath = pathMatch[1];
54
+ filePath = filePath.replace(/^\.\//, '');
55
+ // Extract relative path if absolute
56
+ const parts = filePath.split('/');
57
+ // Keep as-is for now; the overlap check handles it
58
+ files.add(filePath);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ catch {
65
+ // Skip malformed JSONL lines
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ // File read error
71
+ }
72
+ return Array.from(files);
73
+ }
74
+ /**
75
+ * Check if any file from the trace overlaps with changed files.
76
+ * Uses fuzzy matching: a trace file "src/foo.ts" matches changed file "src/foo.ts",
77
+ * and an absolute path "/repo/src/foo.ts" matches "src/foo.ts" by suffix.
78
+ */
79
+ function hasOverlap(traceFiles, changedFiles) {
80
+ for (const tf of traceFiles) {
81
+ for (const cf of changedFiles) {
82
+ if (tf === cf || tf.endsWith('/' + cf) || cf.endsWith('/' + tf)) {
83
+ return true;
84
+ }
85
+ }
86
+ }
87
+ return false;
88
+ }
89
+ /**
90
+ * Matches a complete slash-command block from <command-name> through the last
91
+ * </command-*> tag. The block structure is:
92
+ * <command-name>/foo</command-name>
93
+ * <command-message>...</command-message>
94
+ * <command-args>...</command-args>
95
+ *
96
+ * We match each tag group individually and make message/args optional so partial
97
+ * blocks (e.g., name-only) are still stripped.
98
+ */
99
+ const COMMAND_BLOCK_RE = /<command-name>\s*\/[^<]*<\/command-name>(?:\s*<command-message>[\s\S]*?<\/command-message>)?(?:\s*<command-args>[\s\S]*?<\/command-args>)?\s*/g;
100
+ /**
101
+ * Matches a single <local-command-caveat>...</local-command-caveat> wrapper.
102
+ * Non-greedy so adjacent caveats are matched individually (avoids eating
103
+ * human text between two caveats).
104
+ */
105
+ const CAVEAT_RE = /<local-command-caveat>[\s\S]*?<\/local-command-caveat>\s*/g;
106
+ /**
107
+ * Anchored: matches only when the entire string (after trimming) is one or
108
+ * more command blocks with no surrounding human text.
109
+ */
110
+ const COMMAND_ONLY_RE = /^\s*(?:<command-name>\s*\/[^<]*<\/command-name>(?:\s*<command-message>[\s\S]*?<\/command-message>)?(?:\s*<command-args>[\s\S]*?<\/command-args>)?\s*)+$/;
111
+ /**
112
+ * Strip machine-generated wrappers from a text block so only the
113
+ * human-authored content remains. Removes:
114
+ * - <local-command-caveat>...</local-command-caveat> wrappers (individually)
115
+ * - <command-name>...(closing tag) slash-command blocks
116
+ */
117
+ function stripWrappers(text) {
118
+ return text
119
+ .replace(CAVEAT_RE, '')
120
+ .replace(COMMAND_BLOCK_RE, '')
121
+ .trim();
122
+ }
123
+ /**
124
+ * Returns true if the text consists entirely of slash-command blocks with no
125
+ * surrounding human text. Anchored to avoid false positives when a user
126
+ * message merely quotes or references a command tag.
127
+ */
128
+ function isSlashCommand(text) {
129
+ return COMMAND_ONLY_RE.test(text);
130
+ }
131
+ /**
132
+ * Extract the human-authored text from a message content value.
133
+ *
134
+ * Content can be a plain string or a block array. For block arrays, we pull
135
+ * only `{ type: "text" }` blocks (skipping tool_result, tool_reference, etc.)
136
+ * and strip machine wrappers from each block individually.
137
+ */
138
+ function extractHumanText(content) {
139
+ if (typeof content === 'string') {
140
+ const stripped = stripWrappers(content);
141
+ if (!stripped || isSlashCommand(stripped))
142
+ return null;
143
+ return stripped;
144
+ }
145
+ if (!Array.isArray(content))
146
+ return null;
147
+ const textParts = [];
148
+ for (const block of content) {
149
+ if (block?.type !== 'text' || typeof block.text !== 'string')
150
+ continue;
151
+ const stripped = stripWrappers(block.text);
152
+ if (stripped && !isSlashCommand(stripped)) {
153
+ textParts.push(stripped);
154
+ }
155
+ }
156
+ return textParts.length > 0 ? textParts.join('\n') : null;
157
+ }
158
+ /**
159
+ * Extract only user-role messages from a JSONL trace file.
160
+ *
161
+ * The raw trace contains tool results, assistant reasoning, and tool_use blocks
162
+ * that can bloat a single session to 10MB+. The intent-drift checker only needs
163
+ * the user's actual messages to judge whether the agent fulfilled the request.
164
+ *
165
+ * Filters out:
166
+ * - tool_result / tool_reference blocks (machine output, not user intent)
167
+ * - All slash-command messages (not just /clear)
168
+ * - local-command-caveat wrappers (stripped per-block for arrays)
169
+ */
170
+ /**
171
+ * Parse JSONL content that may contain multi-line JSON entries.
172
+ *
173
+ * Tries each line as a standalone JSON object first (fast path). On parse
174
+ * failure, accumulates subsequent lines until a valid object is formed or
175
+ * a cap is hit, so multi-line entries aren't silently dropped.
176
+ */
177
+ function parseJsonl(content, tracePath) {
178
+ const entries = [];
179
+ const lines = content.split('\n');
180
+ let i = 0;
181
+ while (i < lines.length) {
182
+ const line = lines[i].trim();
183
+ if (!line) {
184
+ i++;
185
+ continue;
186
+ }
187
+ try {
188
+ entries.push(JSON.parse(line));
189
+ i++;
190
+ }
191
+ catch {
192
+ // Accumulate lines for a multi-line entry (cap at 50 lines)
193
+ let accumulated = line;
194
+ let found = false;
195
+ const limit = Math.min(i + 50, lines.length);
196
+ let j = i + 1;
197
+ while (j < limit) {
198
+ accumulated += '\n' + lines[j];
199
+ try {
200
+ entries.push(JSON.parse(accumulated));
201
+ found = true;
202
+ j++;
203
+ break;
204
+ }
205
+ catch {
206
+ j++;
207
+ }
208
+ }
209
+ if (!found) {
210
+ console.error(`[traces] Unparseable JSONL at line ${i + 1} in ${tracePath}`);
211
+ // Only skip the single unparseable line — don't jump to j which
212
+ // would skip valid entries that were probed during accumulation.
213
+ i++;
214
+ }
215
+ else {
216
+ i = j;
217
+ }
218
+ }
219
+ }
220
+ return entries;
221
+ }
222
+ function extractUserMessages(tracePath) {
223
+ const messages = [];
224
+ let content;
225
+ try {
226
+ content = readFileSync(tracePath, 'utf-8');
227
+ }
228
+ catch (err) {
229
+ console.error(`[traces] Failed to read trace file ${tracePath}: ${err}`);
230
+ return messages;
231
+ }
232
+ const entries = parseJsonl(content, tracePath);
233
+ for (const entry of entries) {
234
+ if (entry?.type !== 'user')
235
+ continue;
236
+ const msg = entry.message?.content;
237
+ if (!msg)
238
+ continue;
239
+ const text = extractHumanText(msg);
240
+ if (text) {
241
+ messages.push(text);
242
+ }
243
+ }
244
+ return messages;
245
+ }
246
+ /**
247
+ * Find local Entire CLI trace files relevant to the current diff.
248
+ *
249
+ * Scans .entire/metadata/ for session directories containing full.jsonl,
250
+ * then checks if any files modified in those sessions overlap with the
251
+ * files changed in the current diff.
252
+ *
253
+ * Returns paths to condensed files containing only user messages (not the
254
+ * raw multi-MB traces). This keeps the intent-drift sub-agent focused on
255
+ * user intent without burning its context window on tool outputs.
256
+ *
257
+ * @returns Array of absolute paths to condensed user-messages files
258
+ */
259
+ export function findRelevantTraces(gitRoot, baseBranch) {
260
+ const metadataDir = join(gitRoot, '.entire', 'metadata');
261
+ if (!existsSync(metadataDir)) {
262
+ return [];
263
+ }
264
+ const changedFiles = getChangedFiles(gitRoot, baseBranch);
265
+ if (changedFiles.length === 0) {
266
+ return [];
267
+ }
268
+ const triageDir = join(gitRoot, '.haystack', 'triage', 'traces');
269
+ mkdirSync(triageDir, { recursive: true });
270
+ const relevantTraces = [];
271
+ let sessions;
272
+ try {
273
+ sessions = readdirSync(metadataDir, { withFileTypes: true });
274
+ }
275
+ catch (err) {
276
+ console.error(`[traces] Failed to read metadata directory ${metadataDir}: ${err}`);
277
+ return relevantTraces;
278
+ }
279
+ let traceIndex = 0;
280
+ for (const session of sessions) {
281
+ if (!session.isDirectory())
282
+ continue;
283
+ const tracePath = join(metadataDir, session.name, 'full.jsonl');
284
+ if (!existsSync(tracePath))
285
+ continue;
286
+ const traceFiles = extractFilesFromTrace(tracePath);
287
+ if (hasOverlap(traceFiles, changedFiles)) {
288
+ const userMessages = extractUserMessages(tracePath);
289
+ if (userMessages.length === 0)
290
+ continue;
291
+ // Use a monotonic index prefix to avoid collisions if session names
292
+ // are reused across metadata directories.
293
+ const condensedPath = join(triageDir, `${traceIndex}-${session.name}.txt`);
294
+ traceIndex++;
295
+ try {
296
+ writeFileSync(condensedPath, userMessages.map((m, i) => `--- User message ${i + 1} ---\n${m}`).join('\n\n'));
297
+ relevantTraces.push(condensedPath);
298
+ }
299
+ catch (err) {
300
+ console.error(`[traces] Failed to write condensed trace ${condensedPath}: ${err}`);
301
+ }
302
+ }
303
+ }
304
+ return relevantTraces;
305
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Types for the pre-PR triage system.
3
+ *
4
+ * Sub-agents write results as JSON files matching these schemas.
5
+ * The runner reads and aggregates them.
6
+ */
7
+ export type TriageSeverity = 'error' | 'warning' | 'info';
8
+ export interface TriageIssue {
9
+ file: string;
10
+ line?: number;
11
+ severity: TriageSeverity;
12
+ message: string;
13
+ /** For rules validator: rule ID (e.g., PR001) */
14
+ rule?: string;
15
+ /** For intent drift: drift pattern */
16
+ pattern?: 'intent_drift' | 'incomplete_fulfillment';
17
+ }
18
+ export interface CodeReviewResult {
19
+ checker: 'code-review';
20
+ issues: TriageIssue[];
21
+ summary: string;
22
+ passed: boolean;
23
+ }
24
+ export interface RulesValidatorResult {
25
+ checker: 'rules-validator';
26
+ issues: TriageIssue[];
27
+ summary: string;
28
+ passed: boolean;
29
+ rulesChecked: number;
30
+ }
31
+ export interface IntentDriftResult {
32
+ checker: 'intent-drift';
33
+ issues: TriageIssue[];
34
+ summary: string;
35
+ passed: boolean;
36
+ sessionsChecked: number;
37
+ }
38
+ export type CheckerResult = CodeReviewResult | RulesValidatorResult | IntentDriftResult;
39
+ export interface TriageResult {
40
+ passed: boolean;
41
+ results: CheckerResult[];
42
+ /** Errors from sub-agent crashes or missing output */
43
+ errors: string[];
44
+ /** Total duration in ms */
45
+ duration: number;
46
+ }
47
+ export type AgenticCLI = 'claude' | 'codex' | 'gemini';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Types for the pre-PR triage system.
3
+ *
4
+ * Sub-agents write results as JSON files matching these schemas.
5
+ * The runner reads and aggregates them.
6
+ */
7
+ export {};