@haystackeditor/cli 0.8.1 → 0.10.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 (63) hide show
  1. package/README.md +93 -87
  2. package/dist/assets/hooks/llm-rules-template.md +21 -0
  3. package/dist/assets/hooks/package.json +2 -2
  4. package/dist/assets/hooks/scripts/pre-push.sh +20 -0
  5. package/dist/assets/skills/prepare-haystack.md +323 -0
  6. package/dist/assets/skills/secrets.md +164 -0
  7. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  8. package/dist/assets/skills/setup-haystack.md +639 -0
  9. package/dist/assets/skills/submit.md +154 -0
  10. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  11. package/dist/assets/templates/haystack.yml +193 -0
  12. package/dist/commands/check-pending.d.ts +19 -0
  13. package/dist/commands/check-pending.js +217 -0
  14. package/dist/commands/config.d.ts +13 -21
  15. package/dist/commands/config.js +278 -92
  16. package/dist/commands/dismiss.d.ts +17 -0
  17. package/dist/commands/dismiss.js +201 -0
  18. package/dist/commands/init.js +25 -28
  19. package/dist/commands/install-session-hooks.d.ts +16 -0
  20. package/dist/commands/install-session-hooks.js +302 -0
  21. package/dist/commands/login.js +1 -1
  22. package/dist/commands/policy.d.ts +31 -0
  23. package/dist/commands/policy.js +365 -0
  24. package/dist/commands/pr-status.d.ts +16 -0
  25. package/dist/commands/pr-status.js +188 -0
  26. package/dist/commands/setup.d.ts +13 -0
  27. package/dist/commands/setup.js +496 -0
  28. package/dist/commands/skills.d.ts +2 -2
  29. package/dist/commands/skills.js +51 -186
  30. package/dist/commands/submit.d.ts +23 -0
  31. package/dist/commands/submit.js +456 -0
  32. package/dist/commands/triage.d.ts +16 -0
  33. package/dist/commands/triage.js +354 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.js +344 -4
  36. package/dist/tools/detect.d.ts +50 -0
  37. package/dist/tools/detect.js +853 -0
  38. package/dist/tools/fixtures.d.ts +38 -0
  39. package/dist/tools/fixtures.js +199 -0
  40. package/dist/tools/setup.d.ts +43 -0
  41. package/dist/tools/setup.js +597 -0
  42. package/dist/triage/prompts.d.ts +31 -0
  43. package/dist/triage/prompts.js +296 -0
  44. package/dist/triage/runner.d.ts +21 -0
  45. package/dist/triage/runner.js +339 -0
  46. package/dist/triage/traces.d.ts +20 -0
  47. package/dist/triage/traces.js +305 -0
  48. package/dist/triage/types.d.ts +47 -0
  49. package/dist/triage/types.js +7 -0
  50. package/dist/types.d.ts +1387 -191
  51. package/dist/types.js +254 -2
  52. package/dist/utils/analysis-api.d.ts +108 -0
  53. package/dist/utils/analysis-api.js +194 -0
  54. package/dist/utils/config.js +1 -1
  55. package/dist/utils/git.d.ts +80 -0
  56. package/dist/utils/git.js +302 -0
  57. package/dist/utils/github-api.d.ts +83 -0
  58. package/dist/utils/github-api.js +266 -0
  59. package/dist/utils/pending-state.d.ts +40 -0
  60. package/dist/utils/pending-state.js +86 -0
  61. package/dist/utils/secrets.js +3 -3
  62. package/dist/utils/skill.js +257 -0
  63. package/package.json +11 -9
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Prompt builders for pre-PR triage sub-agents.
3
+ *
4
+ * Each function returns a prompt string that instructs the sub-agent to:
5
+ * 1. Analyze the git diff against the base branch
6
+ * 2. Write structured JSON results to a specific file path
7
+ *
8
+ * The sub-agent uses its own tools (Bash, Read, Grep, etc.) to explore the codebase.
9
+ */
10
+ // ============================================================================
11
+ // JSON schemas (embedded in prompts so sub-agents know what to write)
12
+ // ============================================================================
13
+ const ISSUE_SCHEMA = `{
14
+ "file": "relative/path/to/file.ts",
15
+ "line": 42,
16
+ "severity": "error | warning | info",
17
+ "message": "Description of the issue"
18
+ }`;
19
+ const CODE_REVIEW_SCHEMA = `{
20
+ "checker": "code-review",
21
+ "issues": [${ISSUE_SCHEMA}],
22
+ "summary": "Brief 1-sentence summary",
23
+ "passed": true
24
+ }`;
25
+ const RULES_VALIDATOR_SCHEMA = `{
26
+ "checker": "rules-validator",
27
+ "issues": [
28
+ {
29
+ "file": "relative/path/to/file.ts",
30
+ "line": 42,
31
+ "severity": "error | warning",
32
+ "message": "Description of the violation",
33
+ "rule": "PR001"
34
+ }
35
+ ],
36
+ "summary": "Brief 1-sentence summary",
37
+ "rulesChecked": 3,
38
+ "passed": true
39
+ }`;
40
+ const INTENT_DRIFT_SCHEMA = `{
41
+ "checker": "intent-drift",
42
+ "issues": [
43
+ {
44
+ "file": "relative/path/to/file.ts",
45
+ "line": 42,
46
+ "severity": "error | warning | info",
47
+ "message": "Description of the drift or incomplete fulfillment",
48
+ "pattern": "intent_drift | incomplete_fulfillment"
49
+ }
50
+ ],
51
+ "summary": "Brief 1-sentence summary",
52
+ "sessionsChecked": 2,
53
+ "passed": true
54
+ }`;
55
+ // ============================================================================
56
+ // Prompt builders
57
+ // ============================================================================
58
+ /**
59
+ * Build the code review prompt.
60
+ * Always runs — looks for objective bugs in the diff.
61
+ */
62
+ export function buildCodeReviewPrompt(baseBranch, outputPath, precomputedDiff) {
63
+ const diffSection = precomputedDiff
64
+ ? `## Diff (precomputed)
65
+
66
+ \`\`\`diff
67
+ ${precomputedDiff}
68
+ \`\`\`
69
+
70
+ ## Instructions
71
+
72
+ 1. Review the diff above.
73
+ 2. If you need more context for a specific function, read just that section of the file — do NOT read entire large files.
74
+ 3. Identify only REAL BUGS — things that will definitely crash, produce wrong results, or corrupt data.`
75
+ : `## Instructions
76
+
77
+ 1. Run \`git diff ${baseBranch}...HEAD\` to see the full diff.
78
+ 2. If you need more context for a specific function, read just that section of the file — do NOT read entire large files.
79
+ 3. Identify only REAL BUGS — things that will definitely crash, produce wrong results, or corrupt data.`;
80
+ return `You are a pre-PR code reviewer. Your job is to find OBJECTIVE BUGS in the code changes that will cause incorrect runtime behavior.
81
+
82
+ ${diffSection}
83
+
84
+ ## What to flag
85
+
86
+ - Logic errors (wrong condition, off-by-one, inverted boolean)
87
+ - Null/undefined access that will crash at runtime
88
+ - Type mismatches that cause runtime errors (not just TypeScript warnings)
89
+ - Missing return statements that change behavior
90
+ - Resource leaks (unclosed handles, missing cleanup)
91
+ - Race conditions or concurrency bugs
92
+ - Security vulnerabilities (SQL injection, XSS, command injection)
93
+ - Broken API contracts (wrong argument order, missing required fields)
94
+
95
+ ## What NOT to flag
96
+
97
+ - Style preferences, naming conventions, or formatting
98
+ - "Might be an issue" or "could potentially cause problems" — only flag definite bugs
99
+ - Missing error handling unless it WILL crash (not "should have" error handling)
100
+ - Performance concerns unless they cause functional breakage
101
+ - Code that is ugly but correct
102
+ - Pre-existing issues in unchanged code
103
+
104
+ ## Output
105
+
106
+ Write your results to \`${outputPath}\` as JSON with this exact schema:
107
+
108
+ \`\`\`json
109
+ ${CODE_REVIEW_SCHEMA}
110
+ \`\`\`
111
+
112
+ - Set \`passed\` to \`true\` if no issues found (empty issues array)
113
+ - Set \`passed\` to \`false\` if any issues have severity "error"
114
+ - Keep the summary to 1 sentence
115
+ - You MUST write the result file even if no issues are found
116
+
117
+ Be extremely conservative. False positives waste the developer's time. Only flag things you are highly confident are real bugs.`;
118
+ }
119
+ /**
120
+ * Build the rules validator prompt.
121
+ * Runs if .haystack/pr-rules.yml exists OR agent instruction files are found.
122
+ *
123
+ * @returns The prompt string, or null if no rules content or agent policies provided.
124
+ */
125
+ export function buildRulesValidatorPrompt(baseBranch, rulesYaml, outputPath, agentPolicies, precomputedDiff) {
126
+ const hasRules = rulesYaml.trim().length > 0;
127
+ const hasPolicies = agentPolicies && agentPolicies.length > 0;
128
+ if (!hasRules && !hasPolicies)
129
+ return null;
130
+ let rulesSection = '';
131
+ if (hasRules) {
132
+ rulesSection = `## Structured rules (pr-rules.yml)
133
+
134
+ The following rules are defined in the project's \`.haystack/pr-rules.yml\`:
135
+
136
+ \`\`\`yaml
137
+ ${rulesYaml}
138
+ \`\`\`
139
+
140
+ For each structured rule:
141
+ - Read the rule's \`llm.prompt\` to understand what to look for.
142
+ - If the rule has a \`llm.files\` glob, only check files matching that pattern.
143
+ - If you find a violation, record it with the rule's ID (e.g., "PR001") and severity.
144
+ - Use the rule's \`severity\` field (error or warning) for each violation.
145
+ - Use the rule's \`message\` field as guidance for what the violation description should convey.
146
+
147
+ `;
148
+ }
149
+ let policiesSection = '';
150
+ if (hasPolicies) {
151
+ const policyBlocks = agentPolicies.map(p => `### ${p.filename}
152
+
153
+ \`\`\`
154
+ ${p.content}
155
+ \`\`\``).join('\n\n');
156
+ policiesSection = `## Agent instruction policies
157
+
158
+ The following agent instruction files contain project policies that apply to all code changes.
159
+ Extract ACTIONABLE, CHECKABLE rules from these files — directives like "don't do X", "always do Y",
160
+ "never use Z", "avoid X". Ignore general documentation, architecture descriptions, setup instructions,
161
+ and command references.
162
+
163
+ Pay special attention to policies about:
164
+ - Backwards-compatibility hacks, shims, or legacy framing (in code, tests, comments, and naming)
165
+ - Required tools or workflows (e.g., "always use X instead of Y")
166
+ - Prohibited patterns or anti-patterns
167
+ - Security requirements
168
+
169
+ ${policyBlocks}
170
+
171
+ For each extracted policy violation:
172
+ - Set \`rule\` to the source filename (e.g., "CLAUDE.md") instead of a rule ID.
173
+ - Set \`severity\` to "error" for clear "never"/"don't"/"must not" directives, "warning" for "avoid"/"prefer not".
174
+ - Check test code, comments, and variable/function names — not just production code logic. Backwards-compatibility
175
+ framing in test names (e.g., "test_backward_compat", "legacy") or comments (e.g., "# No X field (legacy)")
176
+ violates policies against backwards-compatibility hacks just as much as production shims do.
177
+
178
+ `;
179
+ }
180
+ const diffInstructions = precomputedDiff
181
+ ? `## Diff (precomputed)
182
+
183
+ \`\`\`diff
184
+ ${precomputedDiff}
185
+ \`\`\`
186
+
187
+ ## Instructions
188
+
189
+ 1. Review the diff above.
190
+ 2. Check ONLY the changed code (lines added or modified in the diff), not pre-existing code.
191
+ 3. If you need more context, read just the relevant section of the file — do NOT read entire large files.`
192
+ : `## Instructions
193
+
194
+ 1. Run \`git diff ${baseBranch}...HEAD\` to see the full diff.
195
+ 2. Check ONLY the changed code (lines added or modified in the diff), not pre-existing code.
196
+ 3. If you need more context, read just the relevant section of the file — do NOT read entire large files.`;
197
+ return `You are a PR rules validator. Your job is to check the code changes against project rules and policies, then flag violations.
198
+
199
+ ${rulesSection}${policiesSection}${diffInstructions}
200
+
201
+ ## Important
202
+
203
+ - Only flag violations in NEW or CHANGED code. Do not flag pre-existing issues.
204
+ - Be precise about file paths and line numbers.
205
+ - Structured pr-rules.yml rules take precedence if they overlap with agent policy directives.
206
+
207
+ ## Output
208
+
209
+ Write your results to \`${outputPath}\` as JSON with this exact schema:
210
+
211
+ \`\`\`json
212
+ ${RULES_VALIDATOR_SCHEMA}
213
+ \`\`\`
214
+
215
+ - Set \`rulesChecked\` to the total number of rules evaluated (structured rules + extracted agent policies)
216
+ - Set \`passed\` to \`true\` if no violations found
217
+ - Set \`passed\` to \`false\` if any violations with severity "error" were found
218
+ - You MUST write the result file even if no violations are found`;
219
+ }
220
+ /**
221
+ * Build the intent drift prompt.
222
+ * Only runs if relevant trace files exist.
223
+ *
224
+ * @returns The prompt string, or null if no trace files provided.
225
+ */
226
+ export function buildIntentDriftPrompt(baseBranch, traceFiles, outputPath, precomputedDiff) {
227
+ if (traceFiles.length === 0)
228
+ return null;
229
+ const traceFileList = traceFiles.map(f => `- \`${f}\``).join('\n');
230
+ return `You are an intent drift detector. Your job is to check whether an AI coding agent faithfully implemented what the user asked for.
231
+
232
+ ## Context
233
+
234
+ This PR was created by an AI coding agent. The agent's session transcripts (traces) are stored locally. You will compare what the user asked the agent to do against what was actually implemented in the diff.
235
+
236
+ ## Instructions
237
+
238
+ 1. Read each of these files (they contain the user's messages extracted from the agent session, one per section):
239
+ ${traceFileList}
240
+
241
+ 2. From each file, identify:
242
+ - The user's original instruction/prompt (the first message)
243
+ - Any follow-up instructions or corrections from the user
244
+
245
+ 3. ${precomputedDiff ? 'Review the diff below' : `Run \`git diff ${baseBranch}...HEAD\``} to see what was actually implemented.
246
+
247
+ 4. Compare the user's intent against the actual implementation. Look for:
248
+
249
+ ### Intent Drift
250
+ The agent implemented something DIFFERENT from what was asked:
251
+ - User says "delay as long as possible" → agent uses a fixed 30s timeout
252
+ - User says "only when X" → agent does it unconditionally
253
+ - User says "use library A" → agent uses library B
254
+ - User says "lazy load" → agent loads eagerly
255
+
256
+ ### Incomplete Fulfillment
257
+ The agent didn't finish everything that was asked:
258
+ - User requested 3 things, agent only did 2
259
+ - Interface fields declared but never populated
260
+ - Functions stubbed with TODO/placeholder comments
261
+ - Agent said "I'll skip X for now" for something the user explicitly requested
262
+ - Tests not written when user asked for tests
263
+
264
+ ## Red flags to search for in the diff
265
+
266
+ - Fixed/hardcoded values where dynamic behavior was requested
267
+ - TODO, FIXME, placeholder, stub comments in new code
268
+ - Empty function bodies or early returns
269
+ - Interface fields that are declared but never assigned anywhere
270
+
271
+ ## Output
272
+
273
+ Write your results to \`${outputPath}\` as JSON with this exact schema:
274
+
275
+ \`\`\`json
276
+ ${INTENT_DRIFT_SCHEMA}
277
+ \`\`\`
278
+
279
+ - Set \`sessionsChecked\` to the number of trace files you read
280
+ - Set \`passed\` to \`true\` if no drift or incomplete fulfillment found
281
+ - Set \`passed\` to \`false\` if any issues with severity "error" were found
282
+ - For each issue, set \`pattern\` to either "intent_drift" or "incomplete_fulfillment"
283
+ - You MUST write the result file even if no issues are found
284
+
285
+ ## Severity guidelines
286
+
287
+ - **error**: Core user intent violated — the main thing they asked for is wrong or missing
288
+ - **warning**: Secondary requirement missed or approximated
289
+ - **info**: Minor simplification that mostly still works as intended${precomputedDiff ? `
290
+
291
+ ## Diff (precomputed)
292
+
293
+ \`\`\`diff
294
+ ${precomputedDiff}
295
+ \`\`\`` : ''}`;
296
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Triage runner — orchestrates parallel sub-agent execution for pre-PR checks.
3
+ *
4
+ * Detects the user's agentic CLI (Claude Code, Codex, Gemini), spawns
5
+ * parallel sub-processes for each checker, and collects structured JSON results.
6
+ */
7
+ import type { AgenticCLI, TriageResult } from './types.js';
8
+ /**
9
+ * Detect which agentic CLI is installed.
10
+ * Returns the first one found, or null if none available.
11
+ */
12
+ export declare function detectAgenticCLI(): AgenticCLI | null;
13
+ /**
14
+ * Run pre-PR triage checks using parallel sub-agent processes.
15
+ *
16
+ * @param gitRoot - Absolute path to the git repository root
17
+ * @param baseBranch - The base branch to diff against (e.g., "main")
18
+ * @returns Aggregated triage results
19
+ * @throws If no agentic CLI is detected
20
+ */
21
+ export declare function runTriage(gitRoot: string, baseBranch: string): Promise<TriageResult>;
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Triage runner — orchestrates parallel sub-agent execution for pre-PR checks.
3
+ *
4
+ * Detects the user's agentic CLI (Claude Code, Codex, Gemini), spawns
5
+ * parallel sub-processes for each checker, and collects structured JSON results.
6
+ */
7
+ import { execSync, spawn } from 'child_process';
8
+ import { existsSync, readFileSync, mkdirSync, rmSync } from 'fs';
9
+ import { join } from 'path';
10
+ import chalk from 'chalk';
11
+ import { buildCodeReviewPrompt, buildRulesValidatorPrompt, buildIntentDriftPrompt } from './prompts.js';
12
+ import { findRelevantTraces } from './traces.js';
13
+ // ============================================================================
14
+ // Constants
15
+ // ============================================================================
16
+ const TRIAGE_DIR = '.haystack/triage';
17
+ const TIMEOUT_MS = 300_000; // 5 minutes per checker
18
+ /** Max agentic turns per checker — kept tight since these are read-only tasks */
19
+ const MAX_TURNS = {
20
+ 'code-review': 10,
21
+ 'rules-validator': 10,
22
+ 'intent-drift': 10,
23
+ };
24
+ // ============================================================================
25
+ // Agent policy file discovery
26
+ // ============================================================================
27
+ /** Well-known agent instruction files, checked in discovery order at repo root. */
28
+ const AGENT_POLICY_FILENAMES = [
29
+ 'CLAUDE.md',
30
+ 'AGENTS.md',
31
+ 'COPILOT.md',
32
+ '.cursorrules',
33
+ join('.github', 'copilot-instructions.md'),
34
+ ];
35
+ /**
36
+ * Discover agent instruction files at the repo root.
37
+ * Returns all that exist with their contents.
38
+ */
39
+ function discoverAgentPolicyFiles(gitRoot) {
40
+ const results = [];
41
+ for (const filename of AGENT_POLICY_FILENAMES) {
42
+ const fullPath = join(gitRoot, filename);
43
+ if (existsSync(fullPath)) {
44
+ const content = readFileSync(fullPath, 'utf-8').trim();
45
+ if (content) {
46
+ results.push({ filename, content });
47
+ }
48
+ }
49
+ }
50
+ return results;
51
+ }
52
+ // ============================================================================
53
+ // CLI detection
54
+ // ============================================================================
55
+ /**
56
+ * Detect which agentic CLI is installed.
57
+ * Returns the first one found, or null if none available.
58
+ */
59
+ export function detectAgenticCLI() {
60
+ const isWindows = process.platform === 'win32';
61
+ const inCodexSession = Boolean(process.env.CODEX_SHELL || process.env.CODEX_THREAD_ID || process.env.CODEX_CI);
62
+ const inClaudeSession = Boolean(process.env.CLAUDE_CODE || process.env.CLAUDECODE);
63
+ const clis = inCodexSession
64
+ ? ['codex', 'claude', 'gemini']
65
+ : inClaudeSession
66
+ ? ['claude', 'codex', 'gemini']
67
+ : ['claude', 'codex', 'gemini'];
68
+ for (const name of clis) {
69
+ try {
70
+ execSync(isWindows ? `where ${name}` : `which ${name}`, { stdio: 'ignore' });
71
+ return name;
72
+ }
73
+ catch {
74
+ // Not installed
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+ /**
80
+ * Build the CLI command + args for the detected agentic tool.
81
+ */
82
+ function buildCommand(cli, prompt, maxTurns) {
83
+ switch (cli) {
84
+ case 'claude':
85
+ return {
86
+ command: 'claude',
87
+ args: [
88
+ '-p', prompt,
89
+ '--dangerously-skip-permissions',
90
+ '--max-turns', String(maxTurns),
91
+ '--allowedTools', 'Bash', 'Read', 'Glob', 'Grep', 'Write',
92
+ ],
93
+ };
94
+ case 'codex':
95
+ return {
96
+ command: 'codex',
97
+ args: ['exec', prompt, '--full-auto', '--ephemeral'],
98
+ };
99
+ case 'gemini':
100
+ return {
101
+ command: 'gemini',
102
+ args: ['-p', prompt, '-y'],
103
+ };
104
+ }
105
+ }
106
+ /**
107
+ * Spawn a sub-agent process and wait for it to complete.
108
+ * Returns the exit code.
109
+ */
110
+ function spawnChecker(cli, config, cwd) {
111
+ return new Promise((resolve) => {
112
+ const { command, args } = buildCommand(cli, config.prompt, config.maxTurns);
113
+ let stdout = '';
114
+ let stderr = '';
115
+ const codexHome = join(cwd, '.haystack', 'triage', 'codex-home');
116
+ if (cli === 'codex') {
117
+ mkdirSync(codexHome, { recursive: true });
118
+ }
119
+ const proc = spawn(command, args, {
120
+ cwd,
121
+ stdio: ['ignore', 'pipe', 'pipe'],
122
+ env: {
123
+ ...process.env,
124
+ CLAUDECODE: undefined,
125
+ CODEX_HOME: cli === 'codex' ? codexHome : process.env.CODEX_HOME,
126
+ },
127
+ });
128
+ proc.stdout?.on('data', (data) => {
129
+ stdout += data.toString();
130
+ });
131
+ proc.stderr?.on('data', (data) => {
132
+ stderr += data.toString();
133
+ });
134
+ // Timeout
135
+ const timer = setTimeout(() => {
136
+ proc.kill('SIGTERM');
137
+ stderr += `\nProcess killed after ${TIMEOUT_MS / 1000}s timeout`;
138
+ }, TIMEOUT_MS);
139
+ proc.on('close', (code) => {
140
+ clearTimeout(timer);
141
+ resolve({ exitCode: code ?? 1, output: `${stdout}\n${stderr}`.trim() });
142
+ });
143
+ proc.on('error', (err) => {
144
+ clearTimeout(timer);
145
+ resolve({ exitCode: 1, output: err.message });
146
+ });
147
+ });
148
+ }
149
+ function summarizeCheckerFailureOutput(output) {
150
+ const lines = output
151
+ .split('\n')
152
+ .map(line => line.trim())
153
+ .filter(Boolean);
154
+ if (lines.length === 0)
155
+ return null;
156
+ const priorityPatterns = [
157
+ /not logged in/i,
158
+ /stream disconnected|error sending request|failed to refresh available models/i,
159
+ /operation not permitted|readonly database|permission denied/i,
160
+ /timeout|timed out/i,
161
+ /unauthorized|forbidden/i,
162
+ /^error:/i,
163
+ /failed/i,
164
+ ];
165
+ let summary = lines[lines.length - 1];
166
+ for (const pattern of priorityPatterns) {
167
+ const match = lines.find(line => pattern.test(line));
168
+ if (match) {
169
+ summary = match;
170
+ break;
171
+ }
172
+ }
173
+ return summary.length > 220
174
+ ? `${summary.slice(0, 217)}...`
175
+ : summary;
176
+ }
177
+ // ============================================================================
178
+ // Result reading
179
+ // ============================================================================
180
+ /**
181
+ * Read and validate a checker result JSON file.
182
+ */
183
+ function readCheckerResult(outputPath, checkerName) {
184
+ if (!existsSync(outputPath)) {
185
+ return null;
186
+ }
187
+ try {
188
+ const content = readFileSync(outputPath, 'utf-8');
189
+ const result = JSON.parse(content);
190
+ // Basic validation
191
+ if (!result.checker || !Array.isArray(result.issues) || typeof result.passed !== 'boolean') {
192
+ return null;
193
+ }
194
+ // Ensure issues have required fields
195
+ result.issues = result.issues.filter((issue) => issue.file && issue.message && issue.severity);
196
+ // Recalculate passed based on issues
197
+ result.passed = !result.issues.some((i) => i.severity === 'error');
198
+ return result;
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ // ============================================================================
205
+ // Main runner
206
+ // ============================================================================
207
+ /**
208
+ * Run pre-PR triage checks using parallel sub-agent processes.
209
+ *
210
+ * @param gitRoot - Absolute path to the git repository root
211
+ * @param baseBranch - The base branch to diff against (e.g., "main")
212
+ * @returns Aggregated triage results
213
+ * @throws If no agentic CLI is detected
214
+ */
215
+ export async function runTriage(gitRoot, baseBranch) {
216
+ const startTime = Date.now();
217
+ // Detect CLI
218
+ const cli = detectAgenticCLI();
219
+ if (!cli) {
220
+ throw new Error('No agentic CLI found. Install Claude Code (claude), Codex CLI (codex), or Gemini CLI (gemini).');
221
+ }
222
+ // Fast-fail in Codex app shells where outbound network is disabled.
223
+ // Spawning nested `codex exec` will fail after retries; skip triage immediately instead.
224
+ const codexNetworkDisabled = process.env.CODEX_SANDBOX_NETWORK_DISABLED === '1';
225
+ if (cli === 'codex' && codexNetworkDisabled) {
226
+ throw new Error('Codex shell has network disabled (CODEX_SANDBOX_NETWORK_DISABLED=1), so triage sub-agents cannot run. Re-run outside this sandbox or use --force.');
227
+ }
228
+ console.log(chalk.dim(` Using: ${cli}`));
229
+ // Prepare output directory
230
+ const triageDir = join(gitRoot, TRIAGE_DIR);
231
+ if (existsSync(triageDir)) {
232
+ rmSync(triageDir, { recursive: true });
233
+ }
234
+ mkdirSync(triageDir, { recursive: true });
235
+ // Pre-compute the diff once so sub-agents don't waste turns running git diff.
236
+ // Includes 10 lines of context around each hunk for reviewability.
237
+ let precomputedDiff = null;
238
+ try {
239
+ const diffOutput = execSync(`git diff ${baseBranch}...HEAD -U10`, { cwd: gitRoot, encoding: 'utf-8', maxBuffer: 5 * 1024 * 1024 }).trim();
240
+ // Only inline if the diff is under 100K chars — beyond that, let the agent
241
+ // run git diff itself (it can paginate or review file-by-file).
242
+ if (diffOutput.length > 0 && diffOutput.length <= 100_000) {
243
+ precomputedDiff = diffOutput;
244
+ }
245
+ }
246
+ catch {
247
+ // git diff failed — agents will run it themselves
248
+ }
249
+ // Build checker configs
250
+ const checkers = [];
251
+ // 1. Code review (always runs)
252
+ const codeReviewOutput = join(triageDir, 'code-review.json');
253
+ checkers.push({
254
+ name: 'code-review',
255
+ prompt: buildCodeReviewPrompt(baseBranch, codeReviewOutput, precomputedDiff),
256
+ outputFile: codeReviewOutput,
257
+ maxTurns: MAX_TURNS['code-review'],
258
+ });
259
+ // 2. Rules validator (if pr-rules.yml or agent instruction files exist)
260
+ const rulesPath = join(gitRoot, '.haystack', 'pr-rules.yml');
261
+ const hasRulesYaml = existsSync(rulesPath);
262
+ const rulesYaml = hasRulesYaml ? readFileSync(rulesPath, 'utf-8') : '';
263
+ const agentPolicyFiles = discoverAgentPolicyFiles(gitRoot);
264
+ if (hasRulesYaml || agentPolicyFiles.length > 0) {
265
+ const rulesValidatorOutput = join(triageDir, 'rules-validator.json');
266
+ const rulesPrompt = buildRulesValidatorPrompt(baseBranch, rulesYaml, rulesValidatorOutput, agentPolicyFiles, precomputedDiff);
267
+ if (rulesPrompt) {
268
+ checkers.push({
269
+ name: 'rules-validator',
270
+ prompt: rulesPrompt,
271
+ outputFile: rulesValidatorOutput,
272
+ maxTurns: MAX_TURNS['rules-validator'],
273
+ });
274
+ }
275
+ }
276
+ // 3. Intent drift (only if relevant trace files exist)
277
+ const traceFiles = findRelevantTraces(gitRoot, baseBranch);
278
+ if (traceFiles.length > 0) {
279
+ const intentDriftOutput = join(triageDir, 'intent-drift.json');
280
+ const driftPrompt = buildIntentDriftPrompt(baseBranch, traceFiles, intentDriftOutput, precomputedDiff);
281
+ if (driftPrompt) {
282
+ checkers.push({
283
+ name: 'intent-drift',
284
+ prompt: driftPrompt,
285
+ outputFile: intentDriftOutput,
286
+ maxTurns: MAX_TURNS['intent-drift'],
287
+ });
288
+ }
289
+ }
290
+ // Log what we're running
291
+ const checkerNames = checkers.map(c => c.name);
292
+ console.log(chalk.dim(` Checkers: ${checkerNames.join(', ')}`));
293
+ if (traceFiles.length > 0) {
294
+ console.log(chalk.dim(` Traces: ${traceFiles.length} relevant session(s) found`));
295
+ }
296
+ console.log(chalk.dim(` Running ${checkers.length} checker(s) in parallel...\n`));
297
+ // Spawn all checkers in parallel
298
+ const spawnResults = await Promise.allSettled(checkers.map(async (checker) => {
299
+ const startLabel = chalk.dim(` [${checker.name}]`);
300
+ console.log(`${startLabel} Starting...`);
301
+ const result = await spawnChecker(cli, checker, gitRoot);
302
+ if (result.exitCode === 0) {
303
+ console.log(`${startLabel} ${chalk.green('Done')}`);
304
+ }
305
+ else {
306
+ console.log(`${startLabel} ${chalk.yellow(`Exited with code ${result.exitCode}`)}`);
307
+ }
308
+ return { checker, result };
309
+ }));
310
+ // Collect results
311
+ const results = [];
312
+ const errors = [];
313
+ for (const spawnResult of spawnResults) {
314
+ if (spawnResult.status === 'rejected') {
315
+ errors.push(`Checker failed: ${spawnResult.reason}`);
316
+ continue;
317
+ }
318
+ const { checker, result } = spawnResult.value;
319
+ const checkerResult = readCheckerResult(checker.outputFile, checker.name);
320
+ if (checkerResult) {
321
+ results.push(checkerResult);
322
+ }
323
+ else {
324
+ // Sub-agent didn't produce valid output
325
+ const failureSummary = summarizeCheckerFailureOutput(result.output);
326
+ const errMsg = result.exitCode !== 0
327
+ ? `${checker.name}: process exited with code ${result.exitCode}${failureSummary ? ` (${failureSummary})` : ''}`
328
+ : `${checker.name}: no valid result file produced`;
329
+ errors.push(errMsg);
330
+ }
331
+ }
332
+ const passed = results.every(r => r.passed) && errors.length === 0;
333
+ return {
334
+ passed,
335
+ results,
336
+ errors,
337
+ duration: Date.now() - startTime,
338
+ };
339
+ }
@@ -0,0 +1,20 @@
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
+ /**
8
+ * Find local Entire CLI trace files relevant to the current diff.
9
+ *
10
+ * Scans .entire/metadata/ for session directories containing full.jsonl,
11
+ * then checks if any files modified in those sessions overlap with the
12
+ * files changed in the current diff.
13
+ *
14
+ * Returns paths to condensed files containing only user messages (not the
15
+ * raw multi-MB traces). This keeps the intent-drift sub-agent focused on
16
+ * user intent without burning its context window on tool outputs.
17
+ *
18
+ * @returns Array of absolute paths to condensed user-messages files
19
+ */
20
+ export declare function findRelevantTraces(gitRoot: string, baseBranch: string): string[];