@haystackeditor/cli 0.15.19 → 0.15.21
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 +100 -1
- package/dist/assets/skills/map-your-system.md +9 -4
- package/dist/commands/ask.js +2 -2
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.js +46 -14
- package/dist/commands/dismiss.d.ts +15 -3
- package/dist/commands/dismiss.js +66 -116
- package/dist/commands/mcp.js +96 -1
- package/dist/commands/policy.js +31 -12
- package/dist/commands/pr-status.d.ts +125 -2
- package/dist/commands/pr-status.js +88 -73
- package/dist/commands/pr.d.ts +1 -7
- package/dist/commands/pr.js +10 -21
- package/dist/commands/request-review.d.ts +12 -1
- package/dist/commands/request-review.js +42 -65
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.js +58 -60
- package/dist/commands/setup.js +4 -1
- package/dist/commands/submit.d.ts +3 -0
- package/dist/commands/submit.js +178 -38
- package/dist/commands/system-map.d.ts +42 -0
- package/dist/commands/system-map.js +228 -0
- package/dist/commands/tokens.js +3 -2
- package/dist/commands/traces.js +2 -2
- package/dist/commands/triage.d.ts +1 -6
- package/dist/commands/triage.js +51 -55
- package/dist/commands/verify-core.d.ts +424 -0
- package/dist/commands/verify-core.js +753 -0
- package/dist/commands/verify-mcp.d.ts +8 -0
- package/dist/commands/verify-mcp.js +458 -0
- package/dist/commands/verify-ops.d.ts +158 -0
- package/dist/commands/verify-ops.js +1148 -0
- package/dist/commands/verify-reseal.d.ts +9 -0
- package/dist/commands/verify-reseal.js +148 -0
- package/dist/commands/verify-sandboxes.d.ts +95 -0
- package/dist/commands/verify-sandboxes.js +352 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.js +97 -0
- package/dist/index.js +343 -20
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +4 -0
- package/dist/states.d.ts +29 -0
- package/dist/states.js +46 -0
- package/dist/triage/runner.d.ts +7 -0
- package/dist/triage/runner.js +2 -2
- package/dist/types.d.ts +54 -0
- package/dist/types.js +15 -0
- package/dist/utils/action-output.d.ts +24 -0
- package/dist/utils/action-output.js +26 -0
- package/dist/utils/analysis-api.d.ts +11 -0
- package/dist/utils/analysis-api.js +29 -3
- package/dist/utils/auth.js +7 -0
- package/dist/utils/git.d.ts +4 -1
- package/dist/utils/git.js +17 -7
- package/dist/utils/haystack-api.d.ts +15 -0
- package/dist/utils/haystack-api.js +58 -7
- package/dist/utils/pr-ref.d.ts +27 -0
- package/dist/utils/pr-ref.js +56 -0
- package/package.json +2 -2
- package/schemas/action.v1.json +22 -0
- package/schemas/error.v1.json +14 -0
- package/schemas/pr-status.v1.json +61 -0
- package/schemas/pr.v1.json +80 -17
- package/schemas/setup.v1.json +66 -12
- package/schemas/submit.v1.json +88 -0
- package/schemas/triage.v1.json +71 -18
package/dist/commands/mcp.js
CHANGED
|
@@ -6,11 +6,17 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { fetchInboxPayload } from './inbox.js';
|
|
8
8
|
import { fetchPrStatePayload } from './pr.js';
|
|
9
|
-
import {
|
|
9
|
+
import { fetchPrStatusPayload } from './pr-status.js';
|
|
10
|
+
import { fetchTriagePayload, fetchTriageHeadSha } from './triage.js';
|
|
10
11
|
import { fetchAskPayload } from './ask.js';
|
|
11
12
|
import { fetchTracesGetPayload, fetchTracesListPayload } from './traces.js';
|
|
13
|
+
import { performOverride, performUndismiss } from './dismiss.js';
|
|
14
|
+
import { performRequestReview } from './request-review.js';
|
|
12
15
|
import { loadToken } from '../utils/auth.js';
|
|
16
|
+
import { triggerAnalysis } from '../utils/analysis-api.js';
|
|
17
|
+
import { parsePrRef } from '../utils/pr-ref.js';
|
|
13
18
|
import { SCHEMA_VERSIONS } from '../schema.js';
|
|
19
|
+
import { withSchema } from '../schema.js';
|
|
14
20
|
const refInput = {
|
|
15
21
|
type: 'string',
|
|
16
22
|
description: 'PR reference: bare number, owner/repo#number, or GitHub URL.',
|
|
@@ -63,6 +69,43 @@ const TOOLS = [
|
|
|
63
69
|
required: ['ref', 'checkpoint_id'],
|
|
64
70
|
},
|
|
65
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'pr_status',
|
|
74
|
+
description: 'Get which Haystack feed bucket a PR is in (analyzing, good_to_merge, issues, needs_assignment, ...) and why.',
|
|
75
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'dismiss',
|
|
79
|
+
description: 'Dismiss analysis findings for a PR (moves it toward "Good to Merge"). Tied to the current head commit; a new commit invalidates it.',
|
|
80
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'mark_reviewed',
|
|
84
|
+
description: 'Mark human review as not needed for a PR (moves it toward "Good to Merge"). Tied to the current head commit.',
|
|
85
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'undismiss',
|
|
89
|
+
description: 'Undo dismiss/mark-reviewed overrides for a PR, returning it to its original feed bucket.',
|
|
90
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'request_review',
|
|
94
|
+
description: 'Tag a PR as needing HUMAN review (adds haystack:needs-review), optionally requesting a specific GitHub user.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
ref: refInput,
|
|
99
|
+
reviewer: { type: 'string', description: 'Optional GitHub username to request review from.' },
|
|
100
|
+
},
|
|
101
|
+
required: ['ref'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'trigger_review',
|
|
106
|
+
description: 'Trigger a fresh Haystack MACHINE analysis of a PR\'s current head (equivalent to `haystack review`). Returns immediately; poll triage_get for results.',
|
|
107
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
108
|
+
},
|
|
66
109
|
{
|
|
67
110
|
name: 'schema',
|
|
68
111
|
description: 'Return the committed JSON Schema for a Haystack machine response.',
|
|
@@ -123,6 +166,58 @@ export async function runMcpServer() {
|
|
|
123
166
|
limit: typeof input.limit === 'number' ? String(input.limit) : undefined,
|
|
124
167
|
}));
|
|
125
168
|
}
|
|
169
|
+
if (name === 'pr_status') {
|
|
170
|
+
if (typeof input.ref !== 'string')
|
|
171
|
+
return fail('ref required');
|
|
172
|
+
return ok(await fetchPrStatusPayload(input.ref));
|
|
173
|
+
}
|
|
174
|
+
if (name === 'dismiss' || name === 'mark_reviewed') {
|
|
175
|
+
if (typeof input.ref !== 'string')
|
|
176
|
+
return fail('ref required');
|
|
177
|
+
return ok(withSchema('action', await performOverride(input.ref, name === 'dismiss' ? 'findings-dismissed' : 'review-not-needed', name === 'dismiss' ? 'dismiss' : 'mark-reviewed')));
|
|
178
|
+
}
|
|
179
|
+
if (name === 'undismiss') {
|
|
180
|
+
if (typeof input.ref !== 'string')
|
|
181
|
+
return fail('ref required');
|
|
182
|
+
return ok(withSchema('action', await performUndismiss(input.ref)));
|
|
183
|
+
}
|
|
184
|
+
if (name === 'request_review') {
|
|
185
|
+
if (typeof input.ref !== 'string')
|
|
186
|
+
return fail('ref required');
|
|
187
|
+
return ok(withSchema('action', await performRequestReview(input.ref, typeof input.reviewer === 'string' ? input.reviewer : undefined)));
|
|
188
|
+
}
|
|
189
|
+
if (name === 'trigger_review') {
|
|
190
|
+
if (typeof input.ref !== 'string')
|
|
191
|
+
return fail('ref required');
|
|
192
|
+
const pr = parsePrRef(input.ref, 'review');
|
|
193
|
+
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
194
|
+
if (!token)
|
|
195
|
+
return fail('Not authenticated. Run `haystack login` first.');
|
|
196
|
+
const ref = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
197
|
+
// Resolve the current head so ingestion's dedup compares commits — an
|
|
198
|
+
// in-flight run for an OLDER head must be superseded, not reused
|
|
199
|
+
// (same degradation as `haystack review`: continue head-less on failure).
|
|
200
|
+
let headSha;
|
|
201
|
+
try {
|
|
202
|
+
headSha = await fetchTriageHeadSha(pr, token) ?? undefined;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
headSha = undefined;
|
|
206
|
+
}
|
|
207
|
+
const trigger = await triggerAnalysis(pr.owner, pr.repo, pr.prNumber, token, headSha);
|
|
208
|
+
if (trigger.status !== 'started' && trigger.status !== 'already_processing') {
|
|
209
|
+
return fail(trigger.message);
|
|
210
|
+
}
|
|
211
|
+
return ok(withSchema('action', {
|
|
212
|
+
action: 'trigger_review',
|
|
213
|
+
status: 'ok',
|
|
214
|
+
ref,
|
|
215
|
+
detail: 'Analysis triggered; poll triage_get for results.',
|
|
216
|
+
request_id: trigger.requestId,
|
|
217
|
+
run_status: trigger.status,
|
|
218
|
+
next: null,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
126
221
|
if (name === 'schema') {
|
|
127
222
|
const schema = input.name;
|
|
128
223
|
if (!(schema in SCHEMA_VERSIONS))
|
package/dist/commands/policy.js
CHANGED
|
@@ -167,6 +167,14 @@ export async function addPolicy(nameArg) {
|
|
|
167
167
|
policies = parsed.policies;
|
|
168
168
|
instructions = parsed.instructions;
|
|
169
169
|
}
|
|
170
|
+
// Interactive-only command: without a TTY the inquirer prompt would hang a
|
|
171
|
+
// non-interactive caller (e.g. a coding agent) forever.
|
|
172
|
+
if (!process.stdin.isTTY) {
|
|
173
|
+
console.error(chalk.red('\n`haystack policy add` is interactive and requires a terminal.\n' +
|
|
174
|
+
`Non-interactive alternative: edit ${POLICY_PATH} directly ` +
|
|
175
|
+
'(run `haystack policy list` to see the current policies and format).\n'));
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
170
178
|
console.log(chalk.cyan('\n➕ Add Review Policy\n'));
|
|
171
179
|
// Prompt for policy details
|
|
172
180
|
const answers = await inquirer.prompt([
|
|
@@ -253,18 +261,24 @@ export async function removePolicy(name) {
|
|
|
253
261
|
return;
|
|
254
262
|
}
|
|
255
263
|
const removed = policies[index];
|
|
256
|
-
// Confirm removal
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
// Confirm removal. Without a TTY the prompt would hang — the command names
|
|
265
|
+
// an exact policy, so treat the invocation itself as the confirmation.
|
|
266
|
+
if (process.stdin.isTTY) {
|
|
267
|
+
const { confirm } = await inquirer.prompt([
|
|
268
|
+
{
|
|
269
|
+
type: 'confirm',
|
|
270
|
+
name: 'confirm',
|
|
271
|
+
message: `Remove policy "${removed.name}"?`,
|
|
272
|
+
default: false,
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
if (!confirm) {
|
|
276
|
+
console.log(chalk.dim('\nCancelled.\n'));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.error(chalk.dim(`No TTY — removing "${removed.name}" without interactive confirmation.`));
|
|
268
282
|
}
|
|
269
283
|
// Remove and write
|
|
270
284
|
policies = policies.filter((_, i) => i !== index);
|
|
@@ -288,6 +302,11 @@ export async function addInstruction(text) {
|
|
|
288
302
|
}
|
|
289
303
|
let instruction = text?.trim();
|
|
290
304
|
if (!instruction) {
|
|
305
|
+
if (!process.stdin.isTTY) {
|
|
306
|
+
console.error(chalk.red('\nNo instruction text given and no terminal to prompt in.\n' +
|
|
307
|
+
'Pass the text directly: haystack policy add-instruction "Never flag weak test coverage"\n'));
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
291
310
|
const answers = await inquirer.prompt([
|
|
292
311
|
{
|
|
293
312
|
type: 'input',
|
|
@@ -2,20 +2,143 @@
|
|
|
2
2
|
* pr-status command — Show the current Haystack status of a PR.
|
|
3
3
|
*
|
|
4
4
|
* Calls the classify-debug endpoint to determine which inbox bucket the PR
|
|
5
|
-
* is in (analyzing,
|
|
6
|
-
* and displays a human-readable summary.
|
|
5
|
+
* is in (analyzing, auto_fixing, good_to_merge, issues, needs_assignment,
|
|
6
|
+
* etc.) and displays a human-readable summary.
|
|
7
|
+
*
|
|
8
|
+
* `--json` emits the versioned `pr-status` schema (see `haystack schema
|
|
9
|
+
* pr-status`), with all state tokens normalized to the canonical snake_case
|
|
10
|
+
* vocabulary shared by every CLI JSON surface. The server's raw kebab-case
|
|
11
|
+
* classify-debug response is an internal shape and is not exposed.
|
|
7
12
|
*
|
|
8
13
|
* Accepts a PR identifier in several formats:
|
|
9
14
|
* 123 # Uses current repo's owner/repo
|
|
15
|
+
* #123 # PR number with hash
|
|
10
16
|
* owner/repo#123 # Fully qualified
|
|
11
17
|
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
12
18
|
*/
|
|
19
|
+
import { type ParsedPR } from '../utils/pr-ref.js';
|
|
20
|
+
import { type TriageVerdict } from '../states.js';
|
|
13
21
|
export interface PRStatusOptions {
|
|
14
22
|
json?: boolean;
|
|
15
23
|
}
|
|
24
|
+
interface ClassifyResponse {
|
|
25
|
+
prId: string;
|
|
26
|
+
bucket: string | null;
|
|
27
|
+
feedPriority: number | null;
|
|
28
|
+
isInFeed: boolean;
|
|
29
|
+
reason: string;
|
|
30
|
+
inputs: {
|
|
31
|
+
isOpen: boolean;
|
|
32
|
+
isAuthor: boolean;
|
|
33
|
+
author: string;
|
|
34
|
+
currentUsername: string;
|
|
35
|
+
category: string;
|
|
36
|
+
analysisStatus: string;
|
|
37
|
+
analysisVerdict?: string;
|
|
38
|
+
haystackRating?: number;
|
|
39
|
+
needsHumanReview?: boolean;
|
|
40
|
+
hasReviewer: boolean;
|
|
41
|
+
reviewDecision?: string;
|
|
42
|
+
snoozed: boolean;
|
|
43
|
+
dismissed: boolean;
|
|
44
|
+
dismissedAgentAlerts: boolean;
|
|
45
|
+
dismissedNeedsAssignment: boolean;
|
|
46
|
+
manuallyGoodToMerge: boolean;
|
|
47
|
+
hasNeedsReviewLabel: boolean;
|
|
48
|
+
hasFixingLabel: boolean;
|
|
49
|
+
hasNeedsShepherdingLabel: boolean;
|
|
50
|
+
labels: string[];
|
|
51
|
+
};
|
|
52
|
+
stateSyncedAt?: string;
|
|
53
|
+
staleWarning?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface PrStatusPayload extends Record<string, unknown> {
|
|
56
|
+
ref: string;
|
|
57
|
+
bucket: string | null;
|
|
58
|
+
in_feed: boolean | null;
|
|
59
|
+
feed_priority: number | null;
|
|
60
|
+
reason: string;
|
|
61
|
+
analysis: {
|
|
62
|
+
status: string;
|
|
63
|
+
verdict: TriageVerdict | null;
|
|
64
|
+
rating: number | null;
|
|
65
|
+
};
|
|
66
|
+
needs_human_review: boolean | null;
|
|
67
|
+
labels: string[];
|
|
68
|
+
overrides: {
|
|
69
|
+
snoozed: boolean;
|
|
70
|
+
dismissed: boolean;
|
|
71
|
+
dismissed_agent_alerts: boolean;
|
|
72
|
+
dismissed_needs_assignment: boolean;
|
|
73
|
+
manually_good_to_merge: boolean;
|
|
74
|
+
};
|
|
75
|
+
author: string | null;
|
|
76
|
+
is_author: boolean | null;
|
|
77
|
+
is_open: boolean | null;
|
|
78
|
+
state_synced_at: string | null;
|
|
79
|
+
stale_warning: string | null;
|
|
80
|
+
}
|
|
81
|
+
export declare function buildPrStatusPayload(pr: ParsedPR, data: ClassifyResponse): {
|
|
82
|
+
ref: string;
|
|
83
|
+
bucket: string | null;
|
|
84
|
+
in_feed: boolean;
|
|
85
|
+
feed_priority: number | null;
|
|
86
|
+
reason: string;
|
|
87
|
+
analysis: {
|
|
88
|
+
status: string;
|
|
89
|
+
verdict: "good_to_merge" | "needs_review" | "needs_input" | null;
|
|
90
|
+
rating: number | null;
|
|
91
|
+
};
|
|
92
|
+
needs_human_review: boolean | null;
|
|
93
|
+
labels: string[];
|
|
94
|
+
overrides: {
|
|
95
|
+
snoozed: boolean;
|
|
96
|
+
dismissed: boolean;
|
|
97
|
+
dismissed_agent_alerts: boolean;
|
|
98
|
+
dismissed_needs_assignment: boolean;
|
|
99
|
+
manually_good_to_merge: boolean;
|
|
100
|
+
};
|
|
101
|
+
author: string;
|
|
102
|
+
is_author: boolean;
|
|
103
|
+
is_open: boolean;
|
|
104
|
+
state_synced_at: string | null;
|
|
105
|
+
stale_warning: string | null;
|
|
106
|
+
} & {
|
|
107
|
+
schema_version: string;
|
|
108
|
+
};
|
|
16
109
|
export declare const BUCKET_DISPLAY: Record<string, {
|
|
17
110
|
label: string;
|
|
18
111
|
color: (s: string) => string;
|
|
19
112
|
icon: string;
|
|
20
113
|
}>;
|
|
114
|
+
/** Pure read path (also used by MCP): returns the versioned payload without writing stdout. */
|
|
115
|
+
export declare function fetchPrStatusPayload(identifier: string): Promise<{
|
|
116
|
+
ref: string;
|
|
117
|
+
bucket: string | null;
|
|
118
|
+
in_feed: boolean;
|
|
119
|
+
feed_priority: number | null;
|
|
120
|
+
reason: string;
|
|
121
|
+
analysis: {
|
|
122
|
+
status: string;
|
|
123
|
+
verdict: "good_to_merge" | "needs_review" | "needs_input" | null;
|
|
124
|
+
rating: number | null;
|
|
125
|
+
};
|
|
126
|
+
needs_human_review: boolean | null;
|
|
127
|
+
labels: string[];
|
|
128
|
+
overrides: {
|
|
129
|
+
snoozed: boolean;
|
|
130
|
+
dismissed: boolean;
|
|
131
|
+
dismissed_agent_alerts: boolean;
|
|
132
|
+
dismissed_needs_assignment: boolean;
|
|
133
|
+
manually_good_to_merge: boolean;
|
|
134
|
+
};
|
|
135
|
+
author: string;
|
|
136
|
+
is_author: boolean;
|
|
137
|
+
is_open: boolean;
|
|
138
|
+
state_synced_at: string | null;
|
|
139
|
+
stale_warning: string | null;
|
|
140
|
+
} & {
|
|
141
|
+
schema_version: string;
|
|
142
|
+
}>;
|
|
21
143
|
export declare function prStatusCommand(identifier: string, options: PRStatusOptions): Promise<void>;
|
|
144
|
+
export {};
|
|
@@ -2,61 +2,75 @@
|
|
|
2
2
|
* pr-status command — Show the current Haystack status of a PR.
|
|
3
3
|
*
|
|
4
4
|
* Calls the classify-debug endpoint to determine which inbox bucket the PR
|
|
5
|
-
* is in (analyzing,
|
|
6
|
-
* and displays a human-readable summary.
|
|
5
|
+
* is in (analyzing, auto_fixing, good_to_merge, issues, needs_assignment,
|
|
6
|
+
* etc.) and displays a human-readable summary.
|
|
7
|
+
*
|
|
8
|
+
* `--json` emits the versioned `pr-status` schema (see `haystack schema
|
|
9
|
+
* pr-status`), with all state tokens normalized to the canonical snake_case
|
|
10
|
+
* vocabulary shared by every CLI JSON surface. The server's raw kebab-case
|
|
11
|
+
* classify-debug response is an internal shape and is not exposed.
|
|
7
12
|
*
|
|
8
13
|
* Accepts a PR identifier in several formats:
|
|
9
14
|
* 123 # Uses current repo's owner/repo
|
|
15
|
+
* #123 # PR number with hash
|
|
10
16
|
* owner/repo#123 # Fully qualified
|
|
11
17
|
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
12
18
|
*/
|
|
13
19
|
import chalk from 'chalk';
|
|
14
20
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
21
|
+
import { classifyHttpError } from '../utils/haystack-api.js';
|
|
22
|
+
import { parsePrRef } from '../utils/pr-ref.js';
|
|
23
|
+
import { normalizeFeedBucket, toPublicVerdict } from '../states.js';
|
|
24
|
+
import { withSchema } from '../schema.js';
|
|
25
|
+
export function buildPrStatusPayload(pr, data) {
|
|
26
|
+
const inputs = data.inputs ?? {};
|
|
27
|
+
return withSchema('pr-status', {
|
|
28
|
+
ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
|
|
29
|
+
bucket: normalizeFeedBucket(data.bucket),
|
|
30
|
+
in_feed: data.isInFeed ?? null,
|
|
31
|
+
feed_priority: data.feedPriority ?? null,
|
|
32
|
+
reason: data.reason,
|
|
33
|
+
analysis: {
|
|
34
|
+
status: inputs.analysisStatus ?? 'pending',
|
|
35
|
+
// Verdict only exists once analysis is ready; no rating-based guessing here.
|
|
36
|
+
verdict: inputs.analysisVerdict
|
|
37
|
+
? toPublicVerdict(inputs.analysisVerdict, inputs.haystackRating)
|
|
38
|
+
: null,
|
|
39
|
+
rating: inputs.haystackRating ?? null,
|
|
40
|
+
},
|
|
41
|
+
needs_human_review: inputs.needsHumanReview ?? null,
|
|
42
|
+
labels: inputs.labels ?? [],
|
|
43
|
+
overrides: {
|
|
44
|
+
snoozed: inputs.snoozed ?? false,
|
|
45
|
+
dismissed: inputs.dismissed ?? false,
|
|
46
|
+
dismissed_agent_alerts: inputs.dismissedAgentAlerts ?? false,
|
|
47
|
+
dismissed_needs_assignment: inputs.dismissedNeedsAssignment ?? false,
|
|
48
|
+
manually_good_to_merge: inputs.manuallyGoodToMerge ?? false,
|
|
49
|
+
},
|
|
50
|
+
// The classify endpoint can return 200 with missing PR metadata (its own
|
|
51
|
+
// GitHub fetch failed) — coalesce to null so required fields never vanish
|
|
52
|
+
// from the serialized payload (undefined keys are dropped by JSON.stringify).
|
|
53
|
+
author: inputs.author ?? null,
|
|
54
|
+
is_author: inputs.isAuthor ?? null,
|
|
55
|
+
is_open: inputs.isOpen ?? null,
|
|
56
|
+
state_synced_at: data.stateSyncedAt ?? null,
|
|
57
|
+
stale_warning: data.staleWarning ?? null,
|
|
58
|
+
});
|
|
45
59
|
}
|
|
46
60
|
// ============================================================================
|
|
47
61
|
// Display helpers
|
|
48
62
|
// ============================================================================
|
|
49
63
|
export const BUCKET_DISPLAY = {
|
|
50
64
|
'analyzing': { label: 'Analyzing', color: chalk.blue, icon: '...' },
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
65
|
+
'auto_fixing': { label: 'Auto-fixing (alpha)', color: chalk.blue, icon: '>>>' },
|
|
66
|
+
'good_to_merge': { label: 'Good to Merge', color: chalk.green, icon: '[+]' },
|
|
67
|
+
'failed_when_merging': { label: 'Failed When Merging', color: chalk.red, icon: '[!]' },
|
|
54
68
|
'issues': { label: 'Issues Found', color: chalk.yellow, icon: '[?]' },
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
69
|
+
'needs_assignment': { label: 'Needs Reviewer Assignment', color: chalk.yellow, icon: '[?]' },
|
|
70
|
+
'needs_shepherding': { label: 'Needs Manual Merge', color: chalk.yellow, icon: '[!]' },
|
|
71
|
+
'needs_fixing': { label: 'Agent Fixing CI', color: chalk.blue, icon: '>>>' },
|
|
72
|
+
'awaiting_review': { label: 'Awaiting Review', color: chalk.cyan, icon: '[-]' },
|
|
73
|
+
'review_requested': { label: 'Review Requested', color: chalk.cyan, icon: '[-]' },
|
|
60
74
|
'snoozed': { label: 'Snoozed', color: chalk.dim, icon: '[-]' },
|
|
61
75
|
};
|
|
62
76
|
function formatBucket(bucket) {
|
|
@@ -78,10 +92,10 @@ function formatAnalysisStatus(status) {
|
|
|
78
92
|
}
|
|
79
93
|
function formatVerdict(verdict) {
|
|
80
94
|
switch (verdict) {
|
|
81
|
-
case '
|
|
82
|
-
case '
|
|
83
|
-
case '
|
|
84
|
-
default: return chalk.dim(
|
|
95
|
+
case 'good_to_merge': return chalk.green('Good to merge');
|
|
96
|
+
case 'needs_input': return chalk.yellow('Needs input');
|
|
97
|
+
case 'needs_review': return chalk.cyan('Needs review');
|
|
98
|
+
default: return chalk.dim('n/a');
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
// ============================================================================
|
|
@@ -97,24 +111,24 @@ async function fetchClassification(owner, repo, prNumber, token) {
|
|
|
97
111
|
},
|
|
98
112
|
});
|
|
99
113
|
if (!response.ok) {
|
|
100
|
-
|
|
101
|
-
throw new Error('Authentication failed. Run `haystack login` to re-authenticate.');
|
|
102
|
-
}
|
|
103
|
-
if (response.status === 403) {
|
|
104
|
-
throw new Error('Access denied — you may not have permission to this repository.');
|
|
105
|
-
}
|
|
106
|
-
const body = await response.text().catch(() => '');
|
|
107
|
-
throw new Error(`API error (${response.status}): ${body}`);
|
|
114
|
+
throw await classifyHttpError(response, `PR status for ${prId}`);
|
|
108
115
|
}
|
|
109
116
|
return response.json();
|
|
110
117
|
}
|
|
118
|
+
/** Pure read path (also used by MCP): returns the versioned payload without writing stdout. */
|
|
119
|
+
export async function fetchPrStatusPayload(identifier) {
|
|
120
|
+
const pr = parsePrRef(identifier, 'pr-status');
|
|
121
|
+
const authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
122
|
+
const data = await fetchClassification(pr.owner, pr.repo, pr.prNumber, authContext.token);
|
|
123
|
+
return buildPrStatusPayload(pr, data);
|
|
124
|
+
}
|
|
111
125
|
// ============================================================================
|
|
112
126
|
// Command
|
|
113
127
|
// ============================================================================
|
|
114
128
|
export async function prStatusCommand(identifier, options) {
|
|
115
129
|
let pr;
|
|
116
130
|
try {
|
|
117
|
-
pr =
|
|
131
|
+
pr = parsePrRef(identifier, 'pr-status');
|
|
118
132
|
}
|
|
119
133
|
catch (err) {
|
|
120
134
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
@@ -141,52 +155,53 @@ export async function prStatusCommand(identifier, options) {
|
|
|
141
155
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
142
156
|
process.exit(1);
|
|
143
157
|
}
|
|
158
|
+
const payload = buildPrStatusPayload(pr, data);
|
|
144
159
|
// JSON mode — pure JSON to stdout, nothing else
|
|
145
160
|
if (options.json) {
|
|
146
|
-
console.log(JSON.stringify(
|
|
161
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
147
162
|
return;
|
|
148
163
|
}
|
|
149
164
|
// Human-readable output
|
|
150
|
-
console.log(` ${chalk.bold('PR')}: ${
|
|
151
|
-
console.log(` ${chalk.bold('Status')}: ${formatBucket(
|
|
152
|
-
console.log(` ${chalk.bold('Analysis')}: ${formatAnalysisStatus(
|
|
153
|
-
if (
|
|
154
|
-
console.log(` ${chalk.bold('Verdict')}: ${formatVerdict(
|
|
165
|
+
console.log(` ${chalk.bold('PR')}: ${payload.ref}`);
|
|
166
|
+
console.log(` ${chalk.bold('Status')}: ${formatBucket(payload.bucket)}`);
|
|
167
|
+
console.log(` ${chalk.bold('Analysis')}: ${formatAnalysisStatus(payload.analysis.status)}`);
|
|
168
|
+
if (payload.analysis.verdict) {
|
|
169
|
+
console.log(` ${chalk.bold('Verdict')}: ${formatVerdict(payload.analysis.verdict)}`);
|
|
155
170
|
}
|
|
156
|
-
if (
|
|
157
|
-
console.log(` ${chalk.bold('Rating')}: ${
|
|
171
|
+
if (payload.analysis.rating != null) {
|
|
172
|
+
console.log(` ${chalk.bold('Rating')}: ${payload.analysis.rating}/5`);
|
|
158
173
|
}
|
|
159
|
-
console.log(` ${chalk.bold('Reason')}: ${chalk.dim(
|
|
174
|
+
console.log(` ${chalk.bold('Reason')}: ${chalk.dim(payload.reason)}`);
|
|
160
175
|
// Extra context
|
|
161
|
-
if (!
|
|
176
|
+
if (!payload.is_open) {
|
|
162
177
|
console.log(chalk.dim(`\n PR is closed or merged.`));
|
|
163
178
|
}
|
|
164
|
-
if (!
|
|
165
|
-
console.log(chalk.dim(`\n You are not the author (author: ${
|
|
179
|
+
if (!payload.is_author) {
|
|
180
|
+
console.log(chalk.dim(`\n You are not the author (author: ${payload.author}).`));
|
|
166
181
|
}
|
|
167
182
|
// Labels of interest
|
|
168
|
-
const haystackLabels =
|
|
183
|
+
const haystackLabels = payload.labels.filter(l => l.startsWith('haystack:'));
|
|
169
184
|
if (haystackLabels.length > 0) {
|
|
170
185
|
console.log(` ${chalk.bold('Labels')}: ${haystackLabels.join(', ')}`);
|
|
171
186
|
}
|
|
172
187
|
// User overrides
|
|
173
188
|
const overrides = [];
|
|
174
|
-
if (
|
|
189
|
+
if (payload.overrides.dismissed)
|
|
175
190
|
overrides.push('dismissed');
|
|
176
|
-
if (
|
|
191
|
+
if (payload.overrides.dismissed_agent_alerts)
|
|
177
192
|
overrides.push('agent alerts dismissed');
|
|
178
|
-
if (
|
|
193
|
+
if (payload.overrides.dismissed_needs_assignment)
|
|
179
194
|
overrides.push('needs-assignment dismissed');
|
|
180
|
-
if (
|
|
195
|
+
if (payload.overrides.manually_good_to_merge)
|
|
181
196
|
overrides.push('manually marked good-to-merge');
|
|
182
|
-
if (
|
|
197
|
+
if (payload.overrides.snoozed)
|
|
183
198
|
overrides.push('snoozed');
|
|
184
199
|
if (overrides.length > 0) {
|
|
185
200
|
console.log(` ${chalk.bold('Overrides')}: ${overrides.join(', ')}`);
|
|
186
201
|
}
|
|
187
202
|
// Stale warning
|
|
188
|
-
if (
|
|
189
|
-
console.log(chalk.yellow(`\n Warning: ${
|
|
203
|
+
if (payload.stale_warning) {
|
|
204
|
+
console.log(chalk.yellow(`\n Warning: ${payload.stale_warning}`));
|
|
190
205
|
}
|
|
191
206
|
console.log('');
|
|
192
207
|
}
|
package/dist/commands/pr.d.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
owner: string;
|
|
3
|
-
repo: string;
|
|
4
|
-
prNumber: number;
|
|
5
|
-
}
|
|
6
|
-
export declare function parsePrRef(ref: string): ParsedPR;
|
|
1
|
+
export type { ParsedPR } from '../utils/pr-ref.js';
|
|
7
2
|
interface GitHubPr {
|
|
8
3
|
number: number;
|
|
9
4
|
title: string;
|
|
@@ -43,4 +38,3 @@ export declare function fetchPrStatePayload(ref: string): Promise<Record<string,
|
|
|
43
38
|
schema_version: string;
|
|
44
39
|
}>;
|
|
45
40
|
export declare function prReadCommand(ref: string, options: PrReadOptions): Promise<void>;
|
|
46
|
-
export {};
|
package/dist/commands/pr.js
CHANGED
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadToken } from '../utils/auth.js';
|
|
3
3
|
import { fetchAutoFixManifest, fetchRatingSynthesis } from '../utils/analysis-api.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { fetchAnalysisArtifact, fetchGitHubJson, haystackJson, HaystackApiError } from '../utils/haystack-api.js';
|
|
5
|
+
import { parsePrRef } from '../utils/pr-ref.js';
|
|
6
|
+
import { toPublicVerdict } from '../states.js';
|
|
6
7
|
import { withSchema } from '../schema.js';
|
|
7
|
-
export function parsePrRef(ref) {
|
|
8
|
-
const urlMatch = ref.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
|
|
9
|
-
if (urlMatch)
|
|
10
|
-
return { owner: urlMatch[1], repo: urlMatch[2], prNumber: Number.parseInt(urlMatch[3], 10) };
|
|
11
|
-
const qualifiedMatch = ref.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
12
|
-
if (qualifiedMatch)
|
|
13
|
-
return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: Number.parseInt(qualifiedMatch[3], 10) };
|
|
14
|
-
const bare = ref.match(/^#?(\d+)$/);
|
|
15
|
-
if (bare) {
|
|
16
|
-
const remote = parseRemoteUrl('origin');
|
|
17
|
-
return { owner: remote.owner, repo: remote.repo, prNumber: Number.parseInt(bare[1], 10) };
|
|
18
|
-
}
|
|
19
|
-
throw new Error(`Invalid PR reference: "${ref}". Use 123, owner/repo#123, or a GitHub URL.`);
|
|
20
|
-
}
|
|
21
8
|
export function mergePublicState(meta, entry, queueAvailable = true) {
|
|
22
9
|
if (meta.merged) {
|
|
23
10
|
return { status: 'merged', reason: 'The PR has merged.', blocking_checks: [], action: null };
|
|
@@ -58,12 +45,16 @@ export async function fetchMergePublicState(meta, prNumber, owner, repo, token)
|
|
|
58
45
|
const mergeQueue = await haystackJson(`/api/merge-queue/status?repo=${encodeURIComponent(`${owner}/${repo}`)}&branch=${encodeURIComponent(meta.base.ref)}`, token);
|
|
59
46
|
return mergePublicState(meta, mergeQueue.prStatuses?.[String(prNumber)]);
|
|
60
47
|
}
|
|
61
|
-
catch {
|
|
48
|
+
catch (err) {
|
|
49
|
+
// Auth failures are actionable, not transient — surface them instead of
|
|
50
|
+
// reporting "temporarily unavailable, retry".
|
|
51
|
+
if (err instanceof HaystackApiError && err.isAuthError)
|
|
52
|
+
throw err;
|
|
62
53
|
return mergePublicState(meta, undefined, false);
|
|
63
54
|
}
|
|
64
55
|
}
|
|
65
56
|
export async function fetchPrStatePayload(ref) {
|
|
66
|
-
const pr = parsePrRef(ref);
|
|
57
|
+
const pr = parsePrRef(ref, 'pr get');
|
|
67
58
|
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
68
59
|
if (!token)
|
|
69
60
|
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
@@ -74,8 +65,6 @@ export async function fetchPrStatePayload(ref) {
|
|
|
74
65
|
fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, 'entire_traces.json', token),
|
|
75
66
|
fetchMergePublicState(meta, pr.prNumber, pr.owner, pr.repo, token),
|
|
76
67
|
]);
|
|
77
|
-
const verdict = synthesis?.analysisVerdict
|
|
78
|
-
?? (synthesis ? (synthesis.haystackRating >= 5 ? 'clean' : 'has-issues') : null);
|
|
79
68
|
const payload = {
|
|
80
69
|
ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
|
|
81
70
|
title: meta.title,
|
|
@@ -83,7 +72,7 @@ export async function fetchPrStatePayload(ref) {
|
|
|
83
72
|
triage: synthesis
|
|
84
73
|
? {
|
|
85
74
|
status: 'complete',
|
|
86
|
-
verdict:
|
|
75
|
+
verdict: toPublicVerdict(synthesis.analysisVerdict, synthesis.haystackRating),
|
|
87
76
|
findings_count: synthesis.synthesisDisplay?.length ?? 0,
|
|
88
77
|
}
|
|
89
78
|
: { status: 'pending', verdict: null, findings_count: 0 },
|