@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.
- package/README.md +105 -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 +56 -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 +25 -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/assets/skills/prepare-haystack.md +323 -0
- package/dist/assets/skills/secrets.md +164 -0
- package/dist/assets/skills/setup-external-sandbox.md +243 -0
- package/dist/assets/skills/setup-haystack.md +639 -0
- package/dist/assets/skills/submit.md +154 -0
- package/dist/assets/templates/CLAUDE.md.snippet +42 -0
- package/dist/assets/templates/haystack.yml +193 -0
- package/dist/commands/check-pending.d.ts +19 -0
- package/dist/commands/check-pending.js +217 -0
- package/dist/commands/config.d.ts +18 -12
- package/dist/commands/config.js +327 -52
- package/dist/commands/hooks.d.ts +17 -0
- package/dist/commands/hooks.js +269 -0
- package/dist/commands/install-session-hooks.d.ts +16 -0
- package/dist/commands/install-session-hooks.js +302 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +365 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +80 -0
- package/dist/commands/submit.d.ts +22 -0
- package/dist/commands/submit.js +428 -0
- package/dist/commands/triage.d.ts +16 -0
- package/dist/commands/triage.js +354 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +317 -2
- package/dist/tools/detect.d.ts +50 -0
- package/dist/tools/detect.js +853 -0
- package/dist/tools/fixtures.d.ts +38 -0
- package/dist/tools/fixtures.js +199 -0
- package/dist/tools/setup.d.ts +43 -0
- package/dist/tools/setup.js +597 -0
- package/dist/triage/prompts.d.ts +31 -0
- package/dist/triage/prompts.js +266 -0
- package/dist/triage/runner.d.ts +21 -0
- package/dist/triage/runner.js +325 -0
- package/dist/triage/traces.d.ts +20 -0
- package/dist/triage/traces.js +305 -0
- package/dist/triage/types.d.ts +47 -0
- package/dist/triage/types.js +7 -0
- package/dist/types.d.ts +1387 -191
- package/dist/types.js +254 -2
- package/dist/utils/analysis-api.d.ts +108 -0
- package/dist/utils/analysis-api.js +194 -0
- package/dist/utils/config.js +1 -1
- package/dist/utils/git.d.ts +80 -0
- package/dist/utils/git.js +302 -0
- package/dist/utils/github-api.d.ts +83 -0
- package/dist/utils/github-api.js +266 -0
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +226 -0
- package/dist/utils/pending-state.d.ts +38 -0
- package/dist/utils/pending-state.js +86 -0
- package/dist/utils/secrets.js +3 -3
- package/dist/utils/skill.d.ts +1 -1
- package/dist/utils/skill.js +658 -1
- 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[];
|