@haystackeditor/cli 0.10.3 → 0.12.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 +15 -16
- package/dist/assets/hooks/agent-context/detect.ts +180 -0
- package/dist/assets/hooks/agent-context/format.ts +1 -0
- package/dist/assets/hooks/agent-context/index.ts +2 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +14 -5
- package/dist/assets/hooks/agent-context/parsers/codex.ts +416 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +1 -0
- package/dist/assets/hooks/agent-context/types.ts +1 -1
- package/dist/assets/hooks/package.json +2 -1
- package/dist/assets/hooks/scripts/pre-commit.sh +80 -2
- package/dist/assets/hooks/scripts/pre-push.sh +1 -1
- package/dist/assets/skills/submit.md +20 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +94 -69
- package/dist/commands/dismiss.js +2 -1
- package/dist/commands/install-session-hooks.d.ts +3 -2
- package/dist/commands/install-session-hooks.js +27 -11
- package/dist/commands/pr-status.d.ts +5 -0
- package/dist/commands/pr-status.js +2 -2
- package/dist/commands/request-review.d.ts +15 -0
- package/dist/commands/request-review.js +95 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +285 -2
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +12 -19
- package/dist/commands/triage.d.ts +16 -4
- package/dist/commands/triage.js +278 -179
- package/dist/index.d.ts +2 -1
- package/dist/index.js +73 -50
- package/dist/triage/prompts.js +13 -5
- package/dist/triage/runner.js +1 -1
- package/dist/triage/traces.js +9 -3
- package/dist/triage/types.d.ts +1 -1
- package/dist/types.d.ts +146 -26
- package/dist/types.js +44 -5
- package/dist/utils/analysis-api.d.ts +16 -0
- package/dist/utils/analysis-api.js +23 -5
- package/dist/utils/telemetry.d.ts +17 -0
- package/dist/utils/telemetry.js +109 -0
- package/package.json +1 -1
- package/dist/commands/check-pending.d.ts +0 -19
- package/dist/commands/check-pending.js +0 -217
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @haystackeditor/cli
|
|
2
2
|
|
|
3
|
-
Set up Haystack for your project. When PRs are opened, Haystack automatically reviews them for bugs,
|
|
3
|
+
Set up Haystack for your project. When PRs are opened, Haystack automatically reviews them for bugs, instruction drift, and rule violations — then routes them to the right place.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -65,10 +65,11 @@ haystack logout
|
|
|
65
65
|
|
|
66
66
|
### `haystack submit`
|
|
67
67
|
|
|
68
|
-
Create a PR from current changes. Runs pre-PR triage (code review, rules validation,
|
|
68
|
+
Create a PR from current changes. Runs pre-PR triage (code review, rules validation, instruction drift), pushes your branch, and opens the PR.
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
haystack submit # Triage -> create PR -> wait for analysis
|
|
72
|
+
haystack submit --auto-fix # Discouraged alpha auto-fix for straightforward mechanical issues
|
|
72
73
|
haystack submit --title "Fix auth" # Custom PR title
|
|
73
74
|
haystack submit --draft # Create as draft PR
|
|
74
75
|
haystack submit --force # Skip triage checks
|
|
@@ -84,30 +85,26 @@ haystack submit --review octocat # Request review from a specific teammate
|
|
|
84
85
|
|
|
85
86
|
When `--review` is used without a username, the PR is labeled `haystack:needs-review` and appears in your team's assignment queue. When a username is provided, that person is also requested as a reviewer on GitHub.
|
|
86
87
|
|
|
88
|
+
The `--auto-fix` flag is an alpha feature and is discouraged by default. It opts into a sandbox agent that attempts straightforward mechanical fixes before surfacing issues in the Feed. For most PRs, prefer plain `haystack submit`.
|
|
89
|
+
|
|
87
90
|
### `haystack triage`
|
|
88
91
|
|
|
89
92
|
View Haystack analysis results for any PR. Shows the same data as the Haystack web feed: rating, verdict, structured findings with details, verified bugs, human review reasons, and agent fix prompts.
|
|
90
93
|
|
|
91
94
|
```bash
|
|
95
|
+
haystack triage # Last submitted PR
|
|
92
96
|
haystack triage 42 # Current repo, PR #42
|
|
93
97
|
haystack triage owner/repo#99 # Fully qualified
|
|
94
98
|
haystack triage https://github.com/o/r/pull/1 # From GitHub URL
|
|
95
99
|
haystack triage 42 --json # Machine-readable JSON output
|
|
96
100
|
haystack triage 42 --no-wait # Don't wait if analysis is pending
|
|
101
|
+
haystack triage --hook # Minimal one-liner (for session hooks)
|
|
102
|
+
haystack triage --clear # Clear pending submit state
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
### `haystack check-pending`
|
|
102
|
-
|
|
103
|
-
Check the status of a previously submitted PR's analysis (from the current repo):
|
|
105
|
+
When called without a PR identifier, checks the last PR submitted via `haystack submit`. The `--hook` flag produces a single-line summary with the Haystack rating and auto-fixer status, designed for session-start hooks.
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
haystack check-pending # Rich terminal output
|
|
107
|
-
haystack check-pending --hook # Minimal output (for session-start hooks)
|
|
108
|
-
haystack check-pending --json # Machine-readable JSON
|
|
109
|
-
haystack check-pending --clear # Clear pending state
|
|
110
|
-
```
|
|
107
|
+
The `--json` output includes `agentFixPrompt` fields -- ready-to-paste instructions for coding agents to fix each finding.
|
|
111
108
|
|
|
112
109
|
### `haystack dismiss`
|
|
113
110
|
|
|
@@ -163,12 +160,14 @@ haystack config auto-merge # Show current status
|
|
|
163
160
|
haystack config auto-merge on # Enable auto-merge
|
|
164
161
|
haystack config auto-merge off # Disable auto-merge
|
|
165
162
|
|
|
166
|
-
# AI reviewer wait list
|
|
163
|
+
# AI reviewer wait list (merge queue waits for ALL configured bots before merging)
|
|
167
164
|
haystack config wait-for-reviewers # Show status
|
|
168
165
|
haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
|
|
169
166
|
haystack config wait-for-reviewers add cursor coderabbit # Add multiple
|
|
170
167
|
haystack config wait-for-reviewers remove cursor # Stop waiting
|
|
171
168
|
haystack config wait-for-reviewers clear # Wait for none
|
|
169
|
+
# Also accepts raw GitHub bot usernames:
|
|
170
|
+
haystack config wait-for-reviewers add cursor-bugbot[bot]
|
|
172
171
|
```
|
|
173
172
|
|
|
174
173
|
### `haystack skills`
|
|
@@ -196,7 +195,7 @@ haystack hooks install --skip-entire # Only install Haystack hooks
|
|
|
196
195
|
haystack hooks status # Check installation status
|
|
197
196
|
haystack hooks update # Update Entire CLI to latest
|
|
198
197
|
|
|
199
|
-
# Session hooks (
|
|
198
|
+
# Session hooks (triage on CLI start)
|
|
200
199
|
haystack hooks install-session # Auto-detect CLIs
|
|
201
200
|
haystack hooks install-session --cli claude # Claude Code only
|
|
202
201
|
haystack hooks install-session --cli all # All detected CLIs
|
|
@@ -244,7 +243,7 @@ The `setup` wizard writes `.haystack.json` to your repos with discovered rules,
|
|
|
244
243
|
1. Run `haystack setup` to configure your repos (or `haystack init` for local-only config)
|
|
245
244
|
2. Install the [Haystack GitHub App](https://haystackeditor.com/github-app)
|
|
246
245
|
3. When PRs are opened, Haystack automatically:
|
|
247
|
-
- Analyzes the code for bugs,
|
|
246
|
+
- Analyzes the code for bugs, instruction drift, and rule violations
|
|
248
247
|
- Reports results on the PR
|
|
249
248
|
- Routes the PR to the right inbox tab (Good to Merge, Issues Found, etc.)
|
|
250
249
|
- If auto-merge is enabled, clean PRs merge automatically
|
|
@@ -61,6 +61,120 @@ function extractSessionId(filePath: string, ext: string): string {
|
|
|
61
61
|
return path.basename(filePath, ext);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function findMostRecentFileRecursive(
|
|
65
|
+
dir: string,
|
|
66
|
+
matches: (filePath: string) => boolean,
|
|
67
|
+
): string | null {
|
|
68
|
+
if (!fs.existsSync(dir)) return null;
|
|
69
|
+
|
|
70
|
+
const stack = [dir];
|
|
71
|
+
let bestPath: string | null = null;
|
|
72
|
+
let bestMtime = 0;
|
|
73
|
+
|
|
74
|
+
while (stack.length > 0) {
|
|
75
|
+
const currentDir = stack.pop();
|
|
76
|
+
if (!currentDir) continue;
|
|
77
|
+
|
|
78
|
+
let entries: fs.Dirent[];
|
|
79
|
+
try {
|
|
80
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
stack.push(fullPath);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.isFile() || !matches(fullPath)) continue;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.statSync(fullPath);
|
|
95
|
+
if (stat.mtimeMs > bestMtime) {
|
|
96
|
+
bestMtime = stat.mtimeMs;
|
|
97
|
+
bestPath = fullPath;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return bestPath;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readCodexSessionRepoPath(filePath: string): string | null {
|
|
109
|
+
let fd: number | undefined;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
fd = fs.openSync(filePath, 'r');
|
|
113
|
+
const buffer = Buffer.alloc(32 * 1024);
|
|
114
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
115
|
+
const header = buffer.toString('utf8', 0, bytesRead);
|
|
116
|
+
|
|
117
|
+
for (const line of header.split('\n')) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (!trimmed) continue;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(trimmed) as {
|
|
123
|
+
type?: string;
|
|
124
|
+
payload?: { cwd?: string };
|
|
125
|
+
};
|
|
126
|
+
if (
|
|
127
|
+
(parsed.type === 'session_meta' || parsed.type === 'turn_context') &&
|
|
128
|
+
typeof parsed.payload?.cwd === 'string'
|
|
129
|
+
) {
|
|
130
|
+
return parsed.payload.cwd;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
} finally {
|
|
139
|
+
if (fd !== undefined) {
|
|
140
|
+
fs.closeSync(fd);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function codexSessionMatchesRepo(
|
|
148
|
+
sessionFilePath: string,
|
|
149
|
+
repoPath: string,
|
|
150
|
+
): boolean {
|
|
151
|
+
const sessionRepoPath = readCodexSessionRepoPath(sessionFilePath);
|
|
152
|
+
return !sessionRepoPath || sessionRepoPath === repoPath;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getCodexSessionRoots(): string[] {
|
|
156
|
+
const overrideDir = process.env.ENTIRE_TEST_CODEX_SESSION_DIR;
|
|
157
|
+
if (overrideDir) return [overrideDir];
|
|
158
|
+
|
|
159
|
+
const codexHome =
|
|
160
|
+
process.env.ENTIRE_TEST_CODEX_HOME ||
|
|
161
|
+
process.env.CODEX_HOME ||
|
|
162
|
+
path.join(os.homedir(), '.codex');
|
|
163
|
+
|
|
164
|
+
return [
|
|
165
|
+
path.join(codexHome, 'sessions'),
|
|
166
|
+
path.join(codexHome, 'archived_sessions'),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function extractCodexSessionId(filePath: string): string {
|
|
171
|
+
const basename = path.basename(filePath);
|
|
172
|
+
const match = basename.match(
|
|
173
|
+
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
|
|
174
|
+
);
|
|
175
|
+
return match ? match[1] : extractSessionId(filePath, '.jsonl');
|
|
176
|
+
}
|
|
177
|
+
|
|
64
178
|
function detectClaude(repoPath: string): DetectionResult {
|
|
65
179
|
const overrideDir = process.env.ENTIRE_TEST_CLAUDE_PROJECT_DIR;
|
|
66
180
|
const sessionDir =
|
|
@@ -119,8 +233,74 @@ function detectOpenCode(repoPath: string): DetectionResult {
|
|
|
119
233
|
};
|
|
120
234
|
}
|
|
121
235
|
|
|
236
|
+
function detectCodex(repoPath: string): DetectionResult {
|
|
237
|
+
const overrideFile = process.env.ENTIRE_TEST_CODEX_SESSION_FILE;
|
|
238
|
+
if (
|
|
239
|
+
overrideFile &&
|
|
240
|
+
isRecent(overrideFile) &&
|
|
241
|
+
codexSessionMatchesRepo(overrideFile, repoPath)
|
|
242
|
+
) {
|
|
243
|
+
return {
|
|
244
|
+
detected: true,
|
|
245
|
+
agent: 'codex',
|
|
246
|
+
sessionFilePath: overrideFile,
|
|
247
|
+
sessionId: extractCodexSessionId(overrideFile),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const sessionRoots = getCodexSessionRoots();
|
|
252
|
+
const threadId = process.env.CODEX_THREAD_ID;
|
|
253
|
+
|
|
254
|
+
if (threadId) {
|
|
255
|
+
for (const sessionRoot of sessionRoots) {
|
|
256
|
+
const sessionFile = findMostRecentFileRecursive(
|
|
257
|
+
sessionRoot,
|
|
258
|
+
(filePath) =>
|
|
259
|
+
filePath.endsWith(`${threadId}.jsonl`) &&
|
|
260
|
+
path.basename(filePath).startsWith('rollout-'),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
sessionFile &&
|
|
265
|
+
isRecent(sessionFile) &&
|
|
266
|
+
codexSessionMatchesRepo(sessionFile, repoPath)
|
|
267
|
+
) {
|
|
268
|
+
return {
|
|
269
|
+
detected: true,
|
|
270
|
+
agent: 'codex',
|
|
271
|
+
sessionFilePath: sessionFile,
|
|
272
|
+
sessionId: extractCodexSessionId(sessionFile),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const sessionRoot of sessionRoots) {
|
|
279
|
+
const sessionFile = findMostRecentFileRecursive(
|
|
280
|
+
sessionRoot,
|
|
281
|
+
(filePath) =>
|
|
282
|
+
filePath.endsWith('.jsonl') &&
|
|
283
|
+
path.basename(filePath).startsWith('rollout-') &&
|
|
284
|
+
isRecent(filePath) &&
|
|
285
|
+
codexSessionMatchesRepo(filePath, repoPath),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
if (sessionFile) {
|
|
289
|
+
return {
|
|
290
|
+
detected: true,
|
|
291
|
+
agent: 'codex',
|
|
292
|
+
sessionFilePath: sessionFile,
|
|
293
|
+
sessionId: extractCodexSessionId(sessionFile),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { detected: false };
|
|
299
|
+
}
|
|
300
|
+
|
|
122
301
|
const detectors: Array<(repoPath: string) => DetectionResult> = [
|
|
123
302
|
detectClaude,
|
|
303
|
+
detectCodex,
|
|
124
304
|
detectGemini,
|
|
125
305
|
detectOpenCode,
|
|
126
306
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { detectAgent } from './detect.js';
|
|
2
2
|
import { ClaudeParser } from './parsers/claude.js';
|
|
3
|
+
import { CodexParser } from './parsers/codex.js';
|
|
3
4
|
import { GeminiParser } from './parsers/gemini.js';
|
|
4
5
|
import { OpenCodeParser } from './parsers/opencode.js';
|
|
5
6
|
import { formatContext } from './format.js';
|
|
@@ -7,6 +8,7 @@ import type { AgentParser } from './types.js';
|
|
|
7
8
|
|
|
8
9
|
const PARSERS: Record<string, AgentParser> = {
|
|
9
10
|
'claude-code': new ClaudeParser(),
|
|
11
|
+
codex: new CodexParser(),
|
|
10
12
|
'gemini-cli': new GeminiParser(),
|
|
11
13
|
opencode: new OpenCodeParser(),
|
|
12
14
|
};
|
|
@@ -25,6 +25,7 @@ interface ClaudeLine {
|
|
|
25
25
|
gitBranch?: string;
|
|
26
26
|
uuid?: string;
|
|
27
27
|
timestamp?: string;
|
|
28
|
+
// Old format: message wrapper with role/content/usage
|
|
28
29
|
message?: {
|
|
29
30
|
id?: string;
|
|
30
31
|
model?: string;
|
|
@@ -37,10 +38,13 @@ interface ClaudeLine {
|
|
|
37
38
|
cache_read_input_tokens?: number;
|
|
38
39
|
};
|
|
39
40
|
};
|
|
41
|
+
// New Entire CLI format: id/content at top level (type is the role)
|
|
42
|
+
id?: string;
|
|
43
|
+
content?: unknown;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
interface ContentBlock {
|
|
43
|
-
type
|
|
47
|
+
type?: string; // Optional: new format blocks may omit type
|
|
44
48
|
text?: string;
|
|
45
49
|
thinking?: string;
|
|
46
50
|
name?: string;
|
|
@@ -68,7 +72,8 @@ function extractTextContent(content: unknown): string {
|
|
|
68
72
|
|
|
69
73
|
const parts: string[] = [];
|
|
70
74
|
for (const block of content as ContentBlock[]) {
|
|
71
|
-
|
|
75
|
+
// Support both old ({type: "text", text}) and new ({id, text}) content blocks
|
|
76
|
+
if (block.text && (block.type === 'text' || !block.type)) {
|
|
72
77
|
parts.push(block.text);
|
|
73
78
|
}
|
|
74
79
|
}
|
|
@@ -143,9 +148,10 @@ export class ClaudeParser implements AgentParser {
|
|
|
143
148
|
const assistantLines = allLines.filter((l) => l.type === 'assistant');
|
|
144
149
|
|
|
145
150
|
// Deduplicate assistant messages by message.id (streaming produces duplicates)
|
|
151
|
+
// Support both old format (line.message.id) and new format (line.id)
|
|
146
152
|
const dedupedAssistant = new Map<string, ClaudeLine>();
|
|
147
153
|
for (const line of assistantLines) {
|
|
148
|
-
const msgId = line.message?.id;
|
|
154
|
+
const msgId = line.message?.id || line.id;
|
|
149
155
|
if (msgId) {
|
|
150
156
|
dedupedAssistant.set(msgId, line);
|
|
151
157
|
}
|
|
@@ -162,17 +168,20 @@ export class ClaudeParser implements AgentParser {
|
|
|
162
168
|
for (let i = 0; i < maxTurns; i++) {
|
|
163
169
|
if (i < userLines.length) {
|
|
164
170
|
const user = userLines[i];
|
|
171
|
+
// Support both old ({message: {content}}) and new ({content}) formats
|
|
172
|
+
const userContent = user.message?.content ?? user.content;
|
|
165
173
|
transcript.push({
|
|
166
174
|
turnIndex: turnIndex++,
|
|
167
175
|
role: 'user',
|
|
168
|
-
content: extractTextContent(
|
|
176
|
+
content: extractTextContent(userContent),
|
|
169
177
|
modifiedFiles: [],
|
|
170
178
|
toolUses: [],
|
|
171
179
|
});
|
|
172
180
|
}
|
|
173
181
|
if (i < uniqueAssistant.length) {
|
|
174
182
|
const asst = uniqueAssistant[i];
|
|
175
|
-
|
|
183
|
+
// Support both old ({message: {content, usage}}) and new ({content}) formats
|
|
184
|
+
const content = asst.message?.content ?? asst.content;
|
|
176
185
|
const usage = asst.message?.usage;
|
|
177
186
|
transcript.push({
|
|
178
187
|
turnIndex: turnIndex++,
|