@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,266 @@
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) {
63
+ 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.
64
+
65
+ ## Instructions
66
+
67
+ 1. Run \`git diff ${baseBranch}...HEAD --name-only\` to see which files changed.
68
+ 2. Run \`git diff ${baseBranch}...HEAD\` to see the full diff. For large diffs, review file by file.
69
+ 3. For each changed file, read the surrounding code if needed to understand context.
70
+ 4. Identify only REAL BUGS — things that will definitely crash, produce wrong results, or corrupt data.
71
+
72
+ ## What to flag
73
+
74
+ - Logic errors (wrong condition, off-by-one, inverted boolean)
75
+ - Null/undefined access that will crash at runtime
76
+ - Type mismatches that cause runtime errors (not just TypeScript warnings)
77
+ - Missing return statements that change behavior
78
+ - Resource leaks (unclosed handles, missing cleanup)
79
+ - Race conditions or concurrency bugs
80
+ - Security vulnerabilities (SQL injection, XSS, command injection)
81
+ - Broken API contracts (wrong argument order, missing required fields)
82
+
83
+ ## What NOT to flag
84
+
85
+ - Style preferences, naming conventions, or formatting
86
+ - "Might be an issue" or "could potentially cause problems" — only flag definite bugs
87
+ - Missing error handling unless it WILL crash (not "should have" error handling)
88
+ - Performance concerns unless they cause functional breakage
89
+ - Code that is ugly but correct
90
+ - Pre-existing issues in unchanged code
91
+
92
+ ## Output
93
+
94
+ Write your results to \`${outputPath}\` as JSON with this exact schema:
95
+
96
+ \`\`\`json
97
+ ${CODE_REVIEW_SCHEMA}
98
+ \`\`\`
99
+
100
+ - Set \`passed\` to \`true\` if no issues found (empty issues array)
101
+ - Set \`passed\` to \`false\` if any issues have severity "error"
102
+ - Keep the summary to 1 sentence
103
+ - You MUST write the result file even if no issues are found
104
+
105
+ Be extremely conservative. False positives waste the developer's time. Only flag things you are highly confident are real bugs.`;
106
+ }
107
+ /**
108
+ * Build the rules validator prompt.
109
+ * Runs if .haystack/pr-rules.yml exists OR agent instruction files are found.
110
+ *
111
+ * @returns The prompt string, or null if no rules content or agent policies provided.
112
+ */
113
+ export function buildRulesValidatorPrompt(baseBranch, rulesYaml, outputPath, agentPolicies) {
114
+ const hasRules = rulesYaml.trim().length > 0;
115
+ const hasPolicies = agentPolicies && agentPolicies.length > 0;
116
+ if (!hasRules && !hasPolicies)
117
+ return null;
118
+ let rulesSection = '';
119
+ if (hasRules) {
120
+ rulesSection = `## Structured rules (pr-rules.yml)
121
+
122
+ The following rules are defined in the project's \`.haystack/pr-rules.yml\`:
123
+
124
+ \`\`\`yaml
125
+ ${rulesYaml}
126
+ \`\`\`
127
+
128
+ For each structured rule:
129
+ - Read the rule's \`llm.prompt\` to understand what to look for.
130
+ - If the rule has a \`llm.files\` glob, only check files matching that pattern.
131
+ - If you find a violation, record it with the rule's ID (e.g., "PR001") and severity.
132
+ - Use the rule's \`severity\` field (error or warning) for each violation.
133
+ - Use the rule's \`message\` field as guidance for what the violation description should convey.
134
+
135
+ `;
136
+ }
137
+ let policiesSection = '';
138
+ if (hasPolicies) {
139
+ const policyBlocks = agentPolicies.map(p => `### ${p.filename}
140
+
141
+ \`\`\`
142
+ ${p.content}
143
+ \`\`\``).join('\n\n');
144
+ policiesSection = `## Agent instruction policies
145
+
146
+ The following agent instruction files contain project policies that apply to all code changes.
147
+ Extract ACTIONABLE, CHECKABLE rules from these files — directives like "don't do X", "always do Y",
148
+ "never use Z", "avoid X". Ignore general documentation, architecture descriptions, setup instructions,
149
+ and command references.
150
+
151
+ Pay special attention to policies about:
152
+ - Backwards-compatibility hacks, shims, or legacy framing (in code, tests, comments, and naming)
153
+ - Required tools or workflows (e.g., "always use X instead of Y")
154
+ - Prohibited patterns or anti-patterns
155
+ - Security requirements
156
+
157
+ ${policyBlocks}
158
+
159
+ For each extracted policy violation:
160
+ - Set \`rule\` to the source filename (e.g., "CLAUDE.md") instead of a rule ID.
161
+ - Set \`severity\` to "error" for clear "never"/"don't"/"must not" directives, "warning" for "avoid"/"prefer not".
162
+ - Check test code, comments, and variable/function names — not just production code logic. Backwards-compatibility
163
+ framing in test names (e.g., "test_backward_compat", "legacy") or comments (e.g., "# No X field (legacy)")
164
+ violates policies against backwards-compatibility hacks just as much as production shims do.
165
+
166
+ `;
167
+ }
168
+ return `You are a PR rules validator. Your job is to check the code changes against project rules and policies, then flag violations.
169
+
170
+ ${rulesSection}${policiesSection}## Instructions
171
+
172
+ 1. Run \`git diff ${baseBranch}...HEAD --name-only\` to see which files changed.
173
+ 2. Run \`git diff ${baseBranch}...HEAD\` to see the full diff.
174
+ 3. Check ONLY the changed code (lines added or modified in the diff), not pre-existing code.
175
+ 4. If you need more context, read the relevant source files.
176
+
177
+ ## Important
178
+
179
+ - Only flag violations in NEW or CHANGED code. Do not flag pre-existing issues.
180
+ - Be precise about file paths and line numbers.
181
+ - Structured pr-rules.yml rules take precedence if they overlap with agent policy directives.
182
+
183
+ ## Output
184
+
185
+ Write your results to \`${outputPath}\` as JSON with this exact schema:
186
+
187
+ \`\`\`json
188
+ ${RULES_VALIDATOR_SCHEMA}
189
+ \`\`\`
190
+
191
+ - Set \`rulesChecked\` to the total number of rules evaluated (structured rules + extracted agent policies)
192
+ - Set \`passed\` to \`true\` if no violations found
193
+ - Set \`passed\` to \`false\` if any violations with severity "error" were found
194
+ - You MUST write the result file even if no violations are found`;
195
+ }
196
+ /**
197
+ * Build the intent drift prompt.
198
+ * Only runs if relevant trace files exist.
199
+ *
200
+ * @returns The prompt string, or null if no trace files provided.
201
+ */
202
+ export function buildIntentDriftPrompt(baseBranch, traceFiles, outputPath) {
203
+ if (traceFiles.length === 0)
204
+ return null;
205
+ const traceFileList = traceFiles.map(f => `- \`${f}\``).join('\n');
206
+ return `You are an intent drift detector. Your job is to check whether an AI coding agent faithfully implemented what the user asked for.
207
+
208
+ ## Context
209
+
210
+ 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.
211
+
212
+ ## Instructions
213
+
214
+ 1. Read each of these files (they contain the user's messages extracted from the agent session, one per section):
215
+ ${traceFileList}
216
+
217
+ 2. From each file, identify:
218
+ - The user's original instruction/prompt (the first message)
219
+ - Any follow-up instructions or corrections from the user
220
+
221
+ 3. Run \`git diff ${baseBranch}...HEAD\` to see what was actually implemented.
222
+
223
+ 4. Compare the user's intent against the actual implementation. Look for:
224
+
225
+ ### Intent Drift
226
+ The agent implemented something DIFFERENT from what was asked:
227
+ - User says "delay as long as possible" → agent uses a fixed 30s timeout
228
+ - User says "only when X" → agent does it unconditionally
229
+ - User says "use library A" → agent uses library B
230
+ - User says "lazy load" → agent loads eagerly
231
+
232
+ ### Incomplete Fulfillment
233
+ The agent didn't finish everything that was asked:
234
+ - User requested 3 things, agent only did 2
235
+ - Interface fields declared but never populated
236
+ - Functions stubbed with TODO/placeholder comments
237
+ - Agent said "I'll skip X for now" for something the user explicitly requested
238
+ - Tests not written when user asked for tests
239
+
240
+ ## Red flags to search for in the diff
241
+
242
+ - Fixed/hardcoded values where dynamic behavior was requested
243
+ - TODO, FIXME, placeholder, stub comments in new code
244
+ - Empty function bodies or early returns
245
+ - Interface fields that are declared but never assigned anywhere
246
+
247
+ ## Output
248
+
249
+ Write your results to \`${outputPath}\` as JSON with this exact schema:
250
+
251
+ \`\`\`json
252
+ ${INTENT_DRIFT_SCHEMA}
253
+ \`\`\`
254
+
255
+ - Set \`sessionsChecked\` to the number of trace files you read
256
+ - Set \`passed\` to \`true\` if no drift or incomplete fulfillment found
257
+ - Set \`passed\` to \`false\` if any issues with severity "error" were found
258
+ - For each issue, set \`pattern\` to either "intent_drift" or "incomplete_fulfillment"
259
+ - You MUST write the result file even if no issues are found
260
+
261
+ ## Severity guidelines
262
+
263
+ - **error**: Core user intent violated — the main thing they asked for is wrong or missing
264
+ - **warning**: Secondary requirement missed or approximated
265
+ - **info**: Minor simplification that mostly still works as intended`;
266
+ }
@@ -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,325 @@
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 */
19
+ const MAX_TURNS = {
20
+ 'code-review': 30,
21
+ 'rules-validator': 30,
22
+ 'intent-drift': 30,
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
+ // Build checker configs
236
+ const checkers = [];
237
+ // 1. Code review (always runs)
238
+ const codeReviewOutput = join(triageDir, 'code-review.json');
239
+ checkers.push({
240
+ name: 'code-review',
241
+ prompt: buildCodeReviewPrompt(baseBranch, codeReviewOutput),
242
+ outputFile: codeReviewOutput,
243
+ maxTurns: MAX_TURNS['code-review'],
244
+ });
245
+ // 2. Rules validator (if pr-rules.yml or agent instruction files exist)
246
+ const rulesPath = join(gitRoot, '.haystack', 'pr-rules.yml');
247
+ const hasRulesYaml = existsSync(rulesPath);
248
+ const rulesYaml = hasRulesYaml ? readFileSync(rulesPath, 'utf-8') : '';
249
+ const agentPolicyFiles = discoverAgentPolicyFiles(gitRoot);
250
+ if (hasRulesYaml || agentPolicyFiles.length > 0) {
251
+ const rulesValidatorOutput = join(triageDir, 'rules-validator.json');
252
+ const rulesPrompt = buildRulesValidatorPrompt(baseBranch, rulesYaml, rulesValidatorOutput, agentPolicyFiles);
253
+ if (rulesPrompt) {
254
+ checkers.push({
255
+ name: 'rules-validator',
256
+ prompt: rulesPrompt,
257
+ outputFile: rulesValidatorOutput,
258
+ maxTurns: MAX_TURNS['rules-validator'],
259
+ });
260
+ }
261
+ }
262
+ // 3. Intent drift (only if relevant trace files exist)
263
+ const traceFiles = findRelevantTraces(gitRoot, baseBranch);
264
+ if (traceFiles.length > 0) {
265
+ const intentDriftOutput = join(triageDir, 'intent-drift.json');
266
+ const driftPrompt = buildIntentDriftPrompt(baseBranch, traceFiles, intentDriftOutput);
267
+ if (driftPrompt) {
268
+ checkers.push({
269
+ name: 'intent-drift',
270
+ prompt: driftPrompt,
271
+ outputFile: intentDriftOutput,
272
+ maxTurns: MAX_TURNS['intent-drift'],
273
+ });
274
+ }
275
+ }
276
+ // Log what we're running
277
+ const checkerNames = checkers.map(c => c.name);
278
+ console.log(chalk.dim(` Checkers: ${checkerNames.join(', ')}`));
279
+ if (traceFiles.length > 0) {
280
+ console.log(chalk.dim(` Traces: ${traceFiles.length} relevant session(s) found`));
281
+ }
282
+ console.log(chalk.dim(` Running ${checkers.length} checker(s) in parallel...\n`));
283
+ // Spawn all checkers in parallel
284
+ const spawnResults = await Promise.allSettled(checkers.map(async (checker) => {
285
+ const startLabel = chalk.dim(` [${checker.name}]`);
286
+ console.log(`${startLabel} Starting...`);
287
+ const result = await spawnChecker(cli, checker, gitRoot);
288
+ if (result.exitCode === 0) {
289
+ console.log(`${startLabel} ${chalk.green('Done')}`);
290
+ }
291
+ else {
292
+ console.log(`${startLabel} ${chalk.yellow(`Exited with code ${result.exitCode}`)}`);
293
+ }
294
+ return { checker, result };
295
+ }));
296
+ // Collect results
297
+ const results = [];
298
+ const errors = [];
299
+ for (const spawnResult of spawnResults) {
300
+ if (spawnResult.status === 'rejected') {
301
+ errors.push(`Checker failed: ${spawnResult.reason}`);
302
+ continue;
303
+ }
304
+ const { checker, result } = spawnResult.value;
305
+ const checkerResult = readCheckerResult(checker.outputFile, checker.name);
306
+ if (checkerResult) {
307
+ results.push(checkerResult);
308
+ }
309
+ else {
310
+ // Sub-agent didn't produce valid output
311
+ const failureSummary = summarizeCheckerFailureOutput(result.output);
312
+ const errMsg = result.exitCode !== 0
313
+ ? `${checker.name}: process exited with code ${result.exitCode}${failureSummary ? ` (${failureSummary})` : ''}`
314
+ : `${checker.name}: no valid result file produced`;
315
+ errors.push(errMsg);
316
+ }
317
+ }
318
+ const passed = results.every(r => r.passed) && errors.length === 0;
319
+ return {
320
+ passed,
321
+ results,
322
+ errors,
323
+ duration: Date.now() - startTime,
324
+ };
325
+ }
@@ -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[];