@haystackeditor/cli 0.15.17 → 0.15.19
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 +45 -6
- package/dist/assets/skills/map-your-system.md +138 -0
- package/dist/commands/ask.d.ts +14 -0
- package/dist/commands/ask.js +20 -0
- package/dist/commands/inbox.d.ts +65 -0
- package/dist/commands/inbox.js +137 -0
- package/dist/commands/mcp.js +88 -134
- package/dist/commands/pr.d.ts +37 -1
- package/dist/commands/pr.js +95 -77
- package/dist/commands/review.d.ts +24 -0
- package/dist/commands/review.js +193 -0
- package/dist/commands/schema-cmd.js +1 -1
- package/dist/commands/skills.js +4 -0
- package/dist/commands/traces.d.ts +48 -0
- package/dist/commands/traces.js +92 -0
- package/dist/commands/triage.d.ts +12 -0
- package/dist/commands/triage.js +90 -58
- package/dist/commands/webhooks.js +3 -6
- package/dist/index.js +79 -148
- package/dist/schema.d.ts +5 -2
- package/dist/schema.js +5 -2
- package/dist/utils/analysis-api.d.ts +40 -0
- package/dist/utils/analysis-api.js +108 -3
- package/dist/utils/haystack-api.d.ts +20 -0
- package/dist/utils/haystack-api.js +66 -0
- package/package.json +4 -4
- package/schemas/ask.v1.json +40 -0
- package/schemas/inbox.v1.json +27 -0
- package/schemas/pr.v2.json +54 -0
- package/schemas/traces.v1.json +53 -0
- package/schemas/triage.v2.json +64 -0
- package/dist/assets/skills/install-verification.md +0 -67
- package/dist/commands/verification.d.ts +0 -21
- package/dist/commands/verification.js +0 -291
- package/dist/verification/contract.d.ts +0 -240
- package/dist/verification/contract.js +0 -115
- package/dist/verification/init.d.ts +0 -10
- package/dist/verification/init.js +0 -243
- package/dist/verification/manifest.d.ts +0 -49
- package/dist/verification/manifest.js +0 -186
- package/dist/verification/runner.d.ts +0 -20
- package/dist/verification/runner.js +0 -102
- package/dist/verification/safety.d.ts +0 -10
- package/dist/verification/safety.js +0 -252
- package/dist/verification/validate.d.ts +0 -12
- package/dist/verification/validate.js +0 -219
package/README.md
CHANGED
|
@@ -69,7 +69,6 @@ Create a PR from current changes. Runs pre-PR triage (code review, rules validat
|
|
|
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
|
|
73
72
|
haystack submit --title "Fix auth" # Custom PR title
|
|
74
73
|
haystack submit --draft # Create as draft PR
|
|
75
74
|
haystack submit --force # Skip triage checks
|
|
@@ -85,8 +84,6 @@ haystack submit --review octocat # Request review from a specific teammate
|
|
|
85
84
|
|
|
86
85
|
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.
|
|
87
86
|
|
|
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
|
-
|
|
90
87
|
### `haystack triage`
|
|
91
88
|
|
|
92
89
|
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.
|
|
@@ -102,9 +99,51 @@ haystack triage --hook # Minimal one-liner (for session hoo
|
|
|
102
99
|
haystack triage --clear # Clear pending submit state
|
|
103
100
|
```
|
|
104
101
|
|
|
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
|
|
102
|
+
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, designed for session-start hooks.
|
|
103
|
+
|
|
104
|
+
The `--json` output includes every finding and its customer-facing `suggested_fix` when available.
|
|
105
|
+
|
|
106
|
+
### `haystack inbox list`
|
|
107
|
+
|
|
108
|
+
List PRs in your Haystack inbox with the reason each PR is present and the next action:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
haystack inbox list
|
|
112
|
+
haystack inbox list --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `haystack pr get`
|
|
116
|
+
|
|
117
|
+
Get the useful state of one PR: triage, minimal merge blockers, and customer-trace availability:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
haystack pr get 42
|
|
121
|
+
haystack pr get owner/repo#42 --json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `haystack ask`
|
|
125
|
+
|
|
126
|
+
Ask Haystack Chat about a PR. Machine output contains the answer and every customer-facing source Chat consulted:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
haystack ask 42 "Why is this finding legitimate?"
|
|
130
|
+
haystack ask owner/repo#42 "Show the implementation" --json
|
|
131
|
+
haystack ask owner/repo#42 "What about its callers?" --session <session-id> --json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Chat may search the complete authorized repository when the changed files are insufficient. Search results and fetched source are included in `evidence`.
|
|
135
|
+
|
|
136
|
+
### `haystack traces`
|
|
137
|
+
|
|
138
|
+
Inspect retained customer-owned Entire checkpoints and transcript chunks:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
haystack traces list owner/repo#42 --json
|
|
142
|
+
haystack traces get owner/repo#42 <checkpoint-id> --json
|
|
143
|
+
haystack traces get owner/repo#42 <checkpoint-id> --cursor 20 --limit 20 --json
|
|
144
|
+
```
|
|
106
145
|
|
|
107
|
-
|
|
146
|
+
These commands expose customer coding-session data, not Haystack's internal analysis-agent execution.
|
|
108
147
|
|
|
109
148
|
### `haystack dismiss`
|
|
110
149
|
|
|
@@ -135,7 +174,7 @@ haystack mark-reviewed acme/widgets#99 # Mark for specific repo
|
|
|
135
174
|
|
|
136
175
|
### `haystack pr-status`
|
|
137
176
|
|
|
138
|
-
Show what bucket a PR is in within the Haystack pipeline (analyzing,
|
|
177
|
+
Show what bucket a PR is in within the Haystack pipeline (analyzing, good-to-merge, issues, needs-assignment, etc.):
|
|
139
178
|
|
|
140
179
|
```bash
|
|
141
180
|
haystack pr-status 42 # Current repo, PR #42
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Map Your System for Haystack QA
|
|
2
|
+
|
|
3
|
+
Create `.haystack/system.yml` — a short file of facts about how this
|
|
4
|
+
repository's system works, so Haystack QA can run it.
|
|
5
|
+
|
|
6
|
+
## Why this file exists
|
|
7
|
+
|
|
8
|
+
When Haystack reviews a risky PR, it doesn't just read the diff — it runs the
|
|
9
|
+
affected code in a sandbox and captures evidence. Haystack decides **what** to
|
|
10
|
+
test from each PR on its own. What it can't always infer is **how your system
|
|
11
|
+
works**: how to install and build it, how services boot, how to know they're
|
|
12
|
+
ready, how to seed data, how to act as a logged-in test user. That is what
|
|
13
|
+
this file provides. Every fact you add converts risks Haystack identified but
|
|
14
|
+
couldn't execute into checks that actually run.
|
|
15
|
+
|
|
16
|
+
## Hard rules
|
|
17
|
+
|
|
18
|
+
1. **Facts only — never test selection.** The file describes how the system
|
|
19
|
+
works. Keys like `scenarios`, `tests`, `skip`, or `focus` are rejected;
|
|
20
|
+
Haystack alone decides what to test.
|
|
21
|
+
2. **Only verified facts.** Run every command yourself before declaring it.
|
|
22
|
+
A wrong fact is worse than a missing one — Haystack tracks which declared
|
|
23
|
+
facts fail and will flag them on PRs.
|
|
24
|
+
3. **No secrets.** Reference environment variable *names* if needed, never
|
|
25
|
+
values. Anything that looks like a credential is rejected.
|
|
26
|
+
4. **Personas are seeded FAKE users** reachable through dev/test-only login
|
|
27
|
+
paths. Never real accounts, never customer data. If a dev login helper
|
|
28
|
+
exists, it must be disabled in production builds.
|
|
29
|
+
5. **Small and true beats big and aspirational.** Ten verified lines are more
|
|
30
|
+
useful than sixty guesses. Omit anything you could not verify.
|
|
31
|
+
|
|
32
|
+
## Steps
|
|
33
|
+
|
|
34
|
+
### 1. Inspect the repository
|
|
35
|
+
|
|
36
|
+
Determine, with evidence from the tree (lockfiles, manifests, scripts,
|
|
37
|
+
docker-compose, CI config, READMEs):
|
|
38
|
+
|
|
39
|
+
- package manager, install command, and any required build/codegen steps
|
|
40
|
+
- each runnable service: start command, port, and how "ready" is observable
|
|
41
|
+
(health endpoint, log line, or a command that exits 0)
|
|
42
|
+
- how to seed and reset local/test data
|
|
43
|
+
- how existing tests authenticate — is there a seeded test user or dev login?
|
|
44
|
+
- the test runner and any test helpers/factories generated tests should reuse
|
|
45
|
+
- external services the code calls, and how development runs without hitting
|
|
46
|
+
them for real (mock fixture, log driver, sandbox mode)
|
|
47
|
+
- where runtime logs are written
|
|
48
|
+
|
|
49
|
+
### 2. Verify each fact by running it
|
|
50
|
+
|
|
51
|
+
From a clean state where possible: run the install, run the build, boot each
|
|
52
|
+
service and confirm its readiness signal, run the seed command, run the login
|
|
53
|
+
helper. Record the exact commands that worked. Drop anything you could not
|
|
54
|
+
make work — do not declare it with a caveat.
|
|
55
|
+
|
|
56
|
+
### 3. Ask the developer only what you cannot determine
|
|
57
|
+
|
|
58
|
+
Typical questions worth asking a human: "Is there a dev-only way to log in as
|
|
59
|
+
a test user?", "Which services must be running for the API to work?", "Is
|
|
60
|
+
there a canonical seed script?". Keep it to the few facts you genuinely
|
|
61
|
+
cannot infer or verify.
|
|
62
|
+
|
|
63
|
+
### 4. Write `.haystack/system.yml`
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
version: 1
|
|
67
|
+
|
|
68
|
+
environment:
|
|
69
|
+
setup:
|
|
70
|
+
- run: "pnpm install --frozen-lockfile"
|
|
71
|
+
- run: "pnpm codegen"
|
|
72
|
+
rationale: "generated types are imported throughout"
|
|
73
|
+
toolchain: { node: "20", pnpm: "10" }
|
|
74
|
+
|
|
75
|
+
services:
|
|
76
|
+
api:
|
|
77
|
+
root: "apps/api" # optional
|
|
78
|
+
boot: "pnpm dev"
|
|
79
|
+
ready: { http: "http://localhost:8080/health" } # or { log_pattern: "listening" } or { command: "pg_isready" }
|
|
80
|
+
env: { APP_ENV: "test" }
|
|
81
|
+
depends_on: [postgres]
|
|
82
|
+
|
|
83
|
+
data:
|
|
84
|
+
seed: "pnpm db:seed --profile=qa"
|
|
85
|
+
reset: "pnpm db:reset"
|
|
86
|
+
personas:
|
|
87
|
+
admin:
|
|
88
|
+
login: "pnpm test:login admin"
|
|
89
|
+
yields: "prints an Authorization header on stdout"
|
|
90
|
+
|
|
91
|
+
observability:
|
|
92
|
+
logs: ["apps/api/log/dev.log"]
|
|
93
|
+
inspect_db: "pnpm db:console --readonly"
|
|
94
|
+
|
|
95
|
+
external_services:
|
|
96
|
+
stripe:
|
|
97
|
+
offline: "repo ships a stripe-mock fixture used by tests"
|
|
98
|
+
must_not_touch_live: true
|
|
99
|
+
|
|
100
|
+
testing:
|
|
101
|
+
runner: "pnpm vitest"
|
|
102
|
+
probe_helpers: "tests/helpers/ has request builders and DB factories"
|
|
103
|
+
|
|
104
|
+
constraints:
|
|
105
|
+
- "full build takes ~6 min; apps/api boots standalone without the web build"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Every section is optional — include only what you verified. All commands run
|
|
109
|
+
from the repo root unless a `cwd`/`root` says otherwise.
|
|
110
|
+
|
|
111
|
+
### 5. Self-check before opening the PR
|
|
112
|
+
|
|
113
|
+
- [ ] `version: 1` is present and the file is valid YAML
|
|
114
|
+
- [ ] no test-selection keys (`scenarios`, `tests`, `skip`, `focus`, ...)
|
|
115
|
+
- [ ] no secret values anywhere (env var names only)
|
|
116
|
+
- [ ] every `run`/`boot`/`login`/`seed` command was executed and worked
|
|
117
|
+
- [ ] every service has a `ready` signal you observed
|
|
118
|
+
- [ ] personas are seeded fake users via dev/test-only paths
|
|
119
|
+
|
|
120
|
+
Haystack re-validates the file on its next QA run and reports precise errors
|
|
121
|
+
if something is wrong.
|
|
122
|
+
|
|
123
|
+
### 6. Open a small, reviewable PR
|
|
124
|
+
|
|
125
|
+
Add only `.haystack/system.yml` (plus a dev login helper if one was genuinely
|
|
126
|
+
needed — dev/test-gated, with a test proving it's off in production). In the
|
|
127
|
+
PR description, list which facts you verified by running them and which came
|
|
128
|
+
from the developer's answers.
|
|
129
|
+
|
|
130
|
+
## What happens next
|
|
131
|
+
|
|
132
|
+
Haystack treats each entry as a claim. When a QA run executes a declared
|
|
133
|
+
command successfully, the fact is marked **verified** with the run's receipt;
|
|
134
|
+
if it fails at a newer commit it is marked **broken**, and Haystack will ask
|
|
135
|
+
about it on a PR instead of trusting it. When QA hits something the map
|
|
136
|
+
doesn't cover, it asks at most a couple of targeted questions on the affected
|
|
137
|
+
PR, each with a ready-to-paste snippet — answering them grows this file over
|
|
138
|
+
time.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface AskOptions {
|
|
2
|
+
json?: boolean;
|
|
3
|
+
session?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AskResponse {
|
|
6
|
+
schema_version: string;
|
|
7
|
+
ref: string;
|
|
8
|
+
head_sha: string;
|
|
9
|
+
answer: string;
|
|
10
|
+
evidence: Array<Record<string, unknown>>;
|
|
11
|
+
session_id: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function fetchAskPayload(ref: string, question: string, options: Pick<AskOptions, 'session'>): Promise<AskResponse>;
|
|
14
|
+
export declare function askHaystackCommand(ref: string, question: string, options: AskOptions): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadToken } from '../utils/auth.js';
|
|
3
|
+
import { postAskHaystack } from '../utils/haystack-api.js';
|
|
4
|
+
import { parsePrRef } from './pr.js';
|
|
5
|
+
export async function fetchAskPayload(ref, question, options) {
|
|
6
|
+
const pr = parsePrRef(ref);
|
|
7
|
+
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
8
|
+
if (!token)
|
|
9
|
+
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
10
|
+
return postAskHaystack(pr.owner, pr.repo, pr.prNumber, question, options.session, token);
|
|
11
|
+
}
|
|
12
|
+
export async function askHaystackCommand(ref, question, options) {
|
|
13
|
+
const response = await fetchAskPayload(ref, question, options);
|
|
14
|
+
if (options.json) {
|
|
15
|
+
console.log(JSON.stringify(response, null, 2));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(`\n${response.answer}\n`);
|
|
19
|
+
console.log(chalk.dim(`${response.evidence.length} source${response.evidence.length === 1 ? '' : 's'} consulted · session ${response.session_id}\n`));
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
interface GitHubPull {
|
|
2
|
+
number: number;
|
|
3
|
+
title: string;
|
|
4
|
+
html_url: string;
|
|
5
|
+
draft?: boolean;
|
|
6
|
+
user: {
|
|
7
|
+
id: number;
|
|
8
|
+
login: string;
|
|
9
|
+
};
|
|
10
|
+
assignees?: Array<{
|
|
11
|
+
id: number;
|
|
12
|
+
login: string;
|
|
13
|
+
}>;
|
|
14
|
+
requested_reviewers?: Array<{
|
|
15
|
+
id: number;
|
|
16
|
+
login: string;
|
|
17
|
+
}>;
|
|
18
|
+
head: {
|
|
19
|
+
sha: string;
|
|
20
|
+
ref: string;
|
|
21
|
+
};
|
|
22
|
+
base: {
|
|
23
|
+
ref: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface InboxCandidate extends GitHubPull {
|
|
27
|
+
owner: string;
|
|
28
|
+
repo: string;
|
|
29
|
+
}
|
|
30
|
+
interface AnalysisSummary {
|
|
31
|
+
analysisStatus?: 'pending' | 'in_queue' | 'ready' | 'error';
|
|
32
|
+
analysisVerdict?: 'clean' | 'has-issues' | 'needs-review';
|
|
33
|
+
haystackRating?: number;
|
|
34
|
+
needsHumanReview?: boolean;
|
|
35
|
+
findingsCount?: number;
|
|
36
|
+
merge?: {
|
|
37
|
+
status: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
blocking_checks: string[];
|
|
40
|
+
action: string | null;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface InboxListOptions {
|
|
44
|
+
json?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function isRelevantToUser(pr: GitHubPull, userId: number): boolean;
|
|
47
|
+
export declare function classify(candidate: InboxCandidate, analysis: AnalysisSummary | undefined): {
|
|
48
|
+
bucket: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
action: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function fetchInboxPayload(token: string): Promise<{
|
|
53
|
+
pull_requests: {
|
|
54
|
+
bucket: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
action: string;
|
|
57
|
+
ref: string;
|
|
58
|
+
title: string;
|
|
59
|
+
head_sha: string;
|
|
60
|
+
}[];
|
|
61
|
+
} & {
|
|
62
|
+
schema_version: string;
|
|
63
|
+
}>;
|
|
64
|
+
export declare function inboxListCommand(options: InboxListOptions): Promise<void>;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadToken } from '../utils/auth.js';
|
|
3
|
+
import { fetchCliIdentity, fetchGitHubJson, fetchHaystackInstallations, haystackJson, } from '../utils/haystack-api.js';
|
|
4
|
+
import { withSchema } from '../schema.js';
|
|
5
|
+
async function fetchOpenPulls(owner, repo, token) {
|
|
6
|
+
const pulls = [];
|
|
7
|
+
for (let page = 1;; page++) {
|
|
8
|
+
const batch = await fetchGitHubJson(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls?state=open&per_page=100&page=${page}`, token);
|
|
9
|
+
pulls.push(...batch);
|
|
10
|
+
if (batch.length < 100)
|
|
11
|
+
return pulls;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isRelevantToUser(pr, userId) {
|
|
15
|
+
return pr.user.id === userId
|
|
16
|
+
|| (pr.assignees ?? []).some((user) => user.id === userId)
|
|
17
|
+
|| (pr.requested_reviewers ?? []).some((user) => user.id === userId);
|
|
18
|
+
}
|
|
19
|
+
async function mapConcurrent(values, concurrency, fn) {
|
|
20
|
+
const results = new Array(values.length);
|
|
21
|
+
let next = 0;
|
|
22
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
23
|
+
while (next < values.length) {
|
|
24
|
+
const index = next++;
|
|
25
|
+
results[index] = await fn(values[index]);
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
export function classify(candidate, analysis) {
|
|
31
|
+
const findingCount = analysis?.findingsCount ?? 0;
|
|
32
|
+
const failedChecks = analysis?.merge?.blocking_checks ?? [];
|
|
33
|
+
if (analysis?.analysisStatus === 'error') {
|
|
34
|
+
return {
|
|
35
|
+
bucket: 'needs_shepherding',
|
|
36
|
+
reason: 'Haystack analysis failed.',
|
|
37
|
+
action: 'Retry the analysis or inspect the failure.',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!analysis || analysis.analysisStatus === 'pending' || analysis.analysisStatus === 'in_queue') {
|
|
41
|
+
return {
|
|
42
|
+
bucket: 'analyzing',
|
|
43
|
+
reason: 'Haystack is still analyzing this PR.',
|
|
44
|
+
action: 'Wait for analysis to complete.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (analysis.analysisVerdict === 'has-issues' || (analysis.haystackRating ?? 5) < 4) {
|
|
48
|
+
return {
|
|
49
|
+
bucket: 'needs_shepherding',
|
|
50
|
+
reason: findingCount > 0
|
|
51
|
+
? `Haystack found ${findingCount} author-actionable finding${findingCount === 1 ? '' : 's'}.`
|
|
52
|
+
: 'Haystack found author-actionable issues.',
|
|
53
|
+
action: 'Read the triage findings and resolve the issues.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (failedChecks.length > 0 || analysis.merge?.status === 'blocked') {
|
|
57
|
+
return {
|
|
58
|
+
bucket: 'needs_shepherding',
|
|
59
|
+
reason: failedChecks.length > 0
|
|
60
|
+
? `${failedChecks.length} required check${failedChecks.length === 1 ? ' is' : 's are'} failing: ${failedChecks.join(', ')}.`
|
|
61
|
+
: (analysis.merge?.reason ?? 'The PR is blocked from merging.'),
|
|
62
|
+
action: analysis.merge?.action ?? 'Resolve the merge blockers.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (analysis.analysisVerdict === 'needs-review' || analysis.needsHumanReview) {
|
|
66
|
+
return {
|
|
67
|
+
bucket: 'needs_review',
|
|
68
|
+
reason: 'Analysis is clean, but review policy requires a human reviewer.',
|
|
69
|
+
action: 'Complete or request the required review.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
bucket: 'good_to_merge',
|
|
74
|
+
reason: 'Haystack found no blocking issues.',
|
|
75
|
+
action: 'No user action is required.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export async function fetchInboxPayload(token) {
|
|
79
|
+
const [identity, installations] = await Promise.all([
|
|
80
|
+
fetchCliIdentity(token),
|
|
81
|
+
fetchHaystackInstallations(token),
|
|
82
|
+
]);
|
|
83
|
+
const repos = [...new Set(installations.flatMap((i) => i.repositories ?? []).map((r) => r.full_name))];
|
|
84
|
+
const batches = await mapConcurrent(repos, 8, async (fullName) => {
|
|
85
|
+
const [owner, repo] = fullName.split('/');
|
|
86
|
+
if (!owner || !repo)
|
|
87
|
+
throw new Error(`Invalid repository returned by Haystack: ${fullName}`);
|
|
88
|
+
const pulls = await fetchOpenPulls(owner, repo, token);
|
|
89
|
+
return pulls
|
|
90
|
+
.filter((pr) => isRelevantToUser(pr, identity.id))
|
|
91
|
+
.map((pr) => ({ ...pr, owner, repo }));
|
|
92
|
+
});
|
|
93
|
+
const candidates = batches.flat();
|
|
94
|
+
const refs = candidates.map((pr) => `${pr.owner}/${pr.repo}#${pr.number}`);
|
|
95
|
+
const analysis = refs.length === 0
|
|
96
|
+
? {}
|
|
97
|
+
: await haystackJson('/api/bulk-analysis-summaries', token, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
prIdentifiers: refs,
|
|
102
|
+
prBaseBranches: Object.fromEntries(candidates.map((pr) => [`${pr.owner}/${pr.repo}#${pr.number}`, pr.base.ref])),
|
|
103
|
+
prHeadShas: Object.fromEntries(candidates.map((pr) => [`${pr.owner}/${pr.repo}#${pr.number}`, pr.head.sha])),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
return withSchema('inbox', {
|
|
107
|
+
pull_requests: candidates.map((pr) => {
|
|
108
|
+
const ref = `${pr.owner}/${pr.repo}#${pr.number}`;
|
|
109
|
+
return {
|
|
110
|
+
ref,
|
|
111
|
+
title: pr.title,
|
|
112
|
+
head_sha: pr.head.sha,
|
|
113
|
+
...classify(pr, analysis[ref]),
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export async function inboxListCommand(options) {
|
|
119
|
+
const token = await loadToken();
|
|
120
|
+
if (!token)
|
|
121
|
+
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
122
|
+
const payload = await fetchInboxPayload(token);
|
|
123
|
+
if (options.json) {
|
|
124
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (payload.pull_requests.length === 0) {
|
|
128
|
+
console.log(chalk.dim('\nNo PRs need your attention.\n'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.log(chalk.bold('\nHaystack inbox\n'));
|
|
132
|
+
for (const pr of payload.pull_requests) {
|
|
133
|
+
console.log(`${chalk.cyan(pr.ref)} ${chalk.bold(pr.title)}`);
|
|
134
|
+
console.log(` ${pr.reason}`);
|
|
135
|
+
console.log(` ${chalk.dim('Next:')} ${pr.action}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|