@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/dist/commands/mcp.js
CHANGED
|
@@ -1,71 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `haystack mcp` — Model Context Protocol server.
|
|
3
|
-
*
|
|
4
|
-
* Exposes Haystack's read surface to Claude Code, Cursor, Codex, etc. as
|
|
5
|
-
* stdio MCP tools. Every tool wraps the same library function that powers
|
|
6
|
-
* the corresponding `--json` CLI command, so an MCP client and a webhook
|
|
7
|
-
* receiver see byte-identical payloads.
|
|
8
|
-
*
|
|
9
|
-
* Tools:
|
|
10
|
-
* triage_pr(ref) same shape as `haystack triage <ref> --json`
|
|
11
|
-
* pr_state(ref) PR state in the Haystack pipeline (schemas/pr.v1.json)
|
|
12
|
-
* schema(name) prints the JSON Schema for a noun (debugging)
|
|
13
|
-
*
|
|
14
|
-
* Auth: inherited from the local CLI (loadToken). MCP clients run as the
|
|
15
|
-
* operator's user, so they get whatever credentials are on disk — same model
|
|
16
|
-
* as `git`, `gh`, etc.
|
|
17
|
-
*/
|
|
18
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
-
import { CallToolRequestSchema, ListToolsRequestSchema
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
21
4
|
import { readFileSync } from 'node:fs';
|
|
22
5
|
import { fileURLToPath } from 'node:url';
|
|
23
6
|
import { dirname, join } from 'node:path';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
7
|
+
import { fetchInboxPayload } from './inbox.js';
|
|
8
|
+
import { fetchPrStatePayload } from './pr.js';
|
|
9
|
+
import { fetchTriagePayload } from './triage.js';
|
|
10
|
+
import { fetchAskPayload } from './ask.js';
|
|
11
|
+
import { fetchTracesGetPayload, fetchTracesListPayload } from './traces.js';
|
|
26
12
|
import { loadToken } from '../utils/auth.js';
|
|
27
|
-
import { SCHEMA_VERSIONS
|
|
13
|
+
import { SCHEMA_VERSIONS } from '../schema.js';
|
|
14
|
+
const refInput = {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'PR reference: bare number, owner/repo#number, or GitHub URL.',
|
|
17
|
+
};
|
|
28
18
|
const TOOLS = [
|
|
29
19
|
{
|
|
30
|
-
name: '
|
|
31
|
-
description: '
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
name: 'inbox_list',
|
|
21
|
+
description: 'List PRs in the user\'s Haystack inbox with the reason and next action.',
|
|
22
|
+
inputSchema: { type: 'object', properties: {} },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'pr_get',
|
|
26
|
+
description: 'Get useful state for one PR: triage, minimal merge blockers, trace availability, and conditional fixer outcome.',
|
|
27
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'triage_get',
|
|
31
|
+
description: 'Get the complete Haystack verdict and every customer-facing finding for one PR.',
|
|
32
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'ask_haystack',
|
|
36
|
+
description: 'Ask Haystack about a PR. Returns the answer and every customer-facing source consulted.',
|
|
34
37
|
inputSchema: {
|
|
35
38
|
type: 'object',
|
|
36
39
|
properties: {
|
|
37
|
-
ref:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
40
|
+
ref: refInput,
|
|
41
|
+
question: { type: 'string', description: 'Question about the pull request.' },
|
|
42
|
+
session_id: { type: 'string', description: 'Optional prior Ask session to continue.' },
|
|
41
43
|
},
|
|
42
|
-
required: ['ref'],
|
|
44
|
+
required: ['ref', 'question'],
|
|
43
45
|
},
|
|
44
46
|
},
|
|
45
47
|
{
|
|
46
|
-
name: '
|
|
47
|
-
description: '
|
|
48
|
+
name: 'traces_list',
|
|
49
|
+
description: 'List retained customer-owned Entire checkpoints for one PR.',
|
|
50
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'traces_get',
|
|
54
|
+
description: 'Get a page of retained customer-owned transcript chunks for one checkpoint.',
|
|
48
55
|
inputSchema: {
|
|
49
56
|
type: 'object',
|
|
50
57
|
properties: {
|
|
51
|
-
ref:
|
|
58
|
+
ref: refInput,
|
|
59
|
+
checkpoint_id: { type: 'string' },
|
|
60
|
+
cursor: { type: 'string' },
|
|
61
|
+
limit: { type: 'integer', minimum: 1, maximum: 100 },
|
|
52
62
|
},
|
|
53
|
-
required: ['ref'],
|
|
63
|
+
required: ['ref', 'checkpoint_id'],
|
|
54
64
|
},
|
|
55
65
|
},
|
|
56
66
|
{
|
|
57
67
|
name: 'schema',
|
|
58
|
-
description: 'Return the JSON Schema for a Haystack
|
|
59
|
-
'Use this to discover the exact shape of triage / setup / pr envelopes.',
|
|
68
|
+
description: 'Return the committed JSON Schema for a Haystack machine response.',
|
|
60
69
|
inputSchema: {
|
|
61
70
|
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
name: {
|
|
64
|
-
type: 'string',
|
|
65
|
-
enum: Object.keys(SCHEMA_VERSIONS),
|
|
66
|
-
description: 'Schema noun.',
|
|
67
|
-
},
|
|
68
|
-
},
|
|
71
|
+
properties: { name: { type: 'string', enum: Object.keys(SCHEMA_VERSIONS) } },
|
|
69
72
|
required: ['name'],
|
|
70
73
|
},
|
|
71
74
|
},
|
|
@@ -73,113 +76,64 @@ const TOOLS = [
|
|
|
73
76
|
async function readSchema(name) {
|
|
74
77
|
const major = SCHEMA_VERSIONS[name].split('.')[0];
|
|
75
78
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
76
|
-
|
|
77
|
-
const p = join(here, '..', '..', 'schemas', `${name}.v${major}.json`);
|
|
78
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
79
|
-
}
|
|
80
|
-
/** Capture process.stdout written by CLI commands and return it. MCP tool
|
|
81
|
-
* callbacks must return their result, not write it. The simplest way to
|
|
82
|
-
* reuse the existing --json printers without refactoring them is to
|
|
83
|
-
* redirect stdout for the duration of the call. */
|
|
84
|
-
async function captureStdout(fn) {
|
|
85
|
-
const origWrite = process.stdout.write.bind(process.stdout);
|
|
86
|
-
let captured = '';
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
-
process.stdout.write = (chunk, encoding, cb) => {
|
|
89
|
-
captured += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
|
|
90
|
-
if (typeof encoding === 'function')
|
|
91
|
-
encoding();
|
|
92
|
-
else if (cb)
|
|
93
|
-
cb();
|
|
94
|
-
return true;
|
|
95
|
-
};
|
|
96
|
-
try {
|
|
97
|
-
await fn();
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
process.stdout.write = origWrite;
|
|
101
|
-
}
|
|
102
|
-
return captured;
|
|
103
|
-
}
|
|
104
|
-
function jsonFromCapture(captured) {
|
|
105
|
-
try {
|
|
106
|
-
return JSON.parse(captured);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return { error: 'tool produced non-JSON output', raw: captured.slice(0, 1000) };
|
|
110
|
-
}
|
|
79
|
+
return JSON.parse(readFileSync(join(here, '..', '..', 'schemas', `${name}.v${major}.json`), 'utf8'));
|
|
111
80
|
}
|
|
112
81
|
export async function runMcpServer() {
|
|
113
|
-
const server = new Server({
|
|
114
|
-
name: 'haystack-cli',
|
|
115
|
-
version: '1.0.0',
|
|
116
|
-
}, {
|
|
117
|
-
capabilities: { tools: {} },
|
|
118
|
-
});
|
|
82
|
+
const server = new Server({ name: 'haystack-cli', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
119
83
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
120
84
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
121
85
|
const { name, arguments: args } = request.params;
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
const fail = (msg) => ({
|
|
126
|
-
isError: true,
|
|
127
|
-
content: [{ type: 'text', text: msg }],
|
|
128
|
-
});
|
|
86
|
+
const input = (args ?? {});
|
|
87
|
+
const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
88
|
+
const fail = (message) => ({ isError: true, content: [{ type: 'text', text: message }] });
|
|
129
89
|
try {
|
|
130
|
-
if (name === '
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
90
|
+
if (name === 'inbox_list') {
|
|
91
|
+
const token = await loadToken();
|
|
92
|
+
if (!token)
|
|
93
|
+
return fail('Not authenticated. Run `haystack login` first.');
|
|
94
|
+
return ok(await fetchInboxPayload(token));
|
|
95
|
+
}
|
|
96
|
+
if (name === 'pr_get') {
|
|
97
|
+
if (typeof input.ref !== 'string')
|
|
133
98
|
return fail('ref required');
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
prNumber: pr.prNumber,
|
|
147
|
-
rating: synthesis.haystackRating,
|
|
148
|
-
autoFixerHandling: manifest?.fixedItems ?? [],
|
|
149
|
-
autoFixerSkipped: manifest?.leftForReview ?? [],
|
|
150
|
-
findings: (synthesis.synthesisDisplay ?? []).map((f) => ({
|
|
151
|
-
category: f.category,
|
|
152
|
-
summary: f.summary,
|
|
153
|
-
detail: f.detail,
|
|
154
|
-
agentFixPrompt: f.agentFixPrompt ?? null,
|
|
155
|
-
source: f.source ?? null,
|
|
156
|
-
})),
|
|
99
|
+
return ok(await fetchPrStatePayload(input.ref));
|
|
100
|
+
}
|
|
101
|
+
if (name === 'triage_get') {
|
|
102
|
+
if (typeof input.ref !== 'string')
|
|
103
|
+
return fail('ref required');
|
|
104
|
+
return ok(await fetchTriagePayload(input.ref));
|
|
105
|
+
}
|
|
106
|
+
if (name === 'ask_haystack') {
|
|
107
|
+
if (typeof input.ref !== 'string' || typeof input.question !== 'string')
|
|
108
|
+
return fail('ref and question required');
|
|
109
|
+
return ok(await fetchAskPayload(input.ref, input.question, {
|
|
110
|
+
session: typeof input.session_id === 'string' ? input.session_id : undefined,
|
|
157
111
|
}));
|
|
158
112
|
}
|
|
159
|
-
if (name === '
|
|
160
|
-
|
|
161
|
-
if (!ref)
|
|
113
|
+
if (name === 'traces_list') {
|
|
114
|
+
if (typeof input.ref !== 'string')
|
|
162
115
|
return fail('ref required');
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
116
|
+
return ok(await fetchTracesListPayload(input.ref));
|
|
117
|
+
}
|
|
118
|
+
if (name === 'traces_get') {
|
|
119
|
+
if (typeof input.ref !== 'string' || typeof input.checkpoint_id !== 'string')
|
|
120
|
+
return fail('ref and checkpoint_id required');
|
|
121
|
+
return ok(await fetchTracesGetPayload(input.ref, input.checkpoint_id, {
|
|
122
|
+
cursor: typeof input.cursor === 'string' ? input.cursor : undefined,
|
|
123
|
+
limit: typeof input.limit === 'number' ? String(input.limit) : undefined,
|
|
124
|
+
}));
|
|
167
125
|
}
|
|
168
126
|
if (name === 'schema') {
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
127
|
+
const schema = input.name;
|
|
128
|
+
if (!(schema in SCHEMA_VERSIONS))
|
|
171
129
|
return fail(`unknown schema; choose one of ${Object.keys(SCHEMA_VERSIONS).join(', ')}`);
|
|
172
|
-
|
|
173
|
-
return ok(await readSchema(n));
|
|
130
|
+
return ok(await readSchema(schema));
|
|
174
131
|
}
|
|
175
132
|
return fail(`unknown tool: ${name}`);
|
|
176
133
|
}
|
|
177
|
-
catch (
|
|
178
|
-
return fail(
|
|
134
|
+
catch (error) {
|
|
135
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
179
136
|
}
|
|
180
137
|
});
|
|
181
|
-
|
|
182
|
-
await server.connect(transport);
|
|
183
|
-
// The server runs until stdin closes. Stdio MCP gives no exit signal beyond
|
|
184
|
-
// that; we don't return.
|
|
138
|
+
await server.connect(new StdioServerTransport());
|
|
185
139
|
}
|
package/dist/commands/pr.d.ts
CHANGED
|
@@ -4,7 +4,43 @@ export interface ParsedPR {
|
|
|
4
4
|
prNumber: number;
|
|
5
5
|
}
|
|
6
6
|
export declare function parsePrRef(ref: string): ParsedPR;
|
|
7
|
+
interface GitHubPr {
|
|
8
|
+
number: number;
|
|
9
|
+
title: string;
|
|
10
|
+
state: 'open' | 'closed';
|
|
11
|
+
merged: boolean;
|
|
12
|
+
mergeable_state?: string;
|
|
13
|
+
head: {
|
|
14
|
+
sha: string;
|
|
15
|
+
ref: string;
|
|
16
|
+
};
|
|
17
|
+
base: {
|
|
18
|
+
ref: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
interface MergeQueueEntry {
|
|
22
|
+
status?: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
failedChecks?: string[];
|
|
25
|
+
actionNeeded?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function mergePublicState(meta: GitHubPr, entry: MergeQueueEntry | undefined, queueAvailable?: boolean): {
|
|
28
|
+
status: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
blocking_checks: string[];
|
|
31
|
+
action: string | null;
|
|
32
|
+
};
|
|
33
|
+
export declare function fetchMergePublicState(meta: GitHubPr, prNumber: number, owner: string, repo: string, token: string): Promise<{
|
|
34
|
+
status: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
blocking_checks: string[];
|
|
37
|
+
action: string | null;
|
|
38
|
+
}>;
|
|
7
39
|
export interface PrReadOptions {
|
|
8
40
|
json?: boolean;
|
|
9
41
|
}
|
|
10
|
-
export declare function
|
|
42
|
+
export declare function fetchPrStatePayload(ref: string): Promise<Record<string, unknown> & {
|
|
43
|
+
schema_version: string;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function prReadCommand(ref: string, options: PrReadOptions): Promise<void>;
|
|
46
|
+
export {};
|
package/dist/commands/pr.js
CHANGED
|
@@ -1,103 +1,121 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR-reference helpers shared by the MCP server's `pr_state` tool.
|
|
3
|
-
*
|
|
4
|
-
* No top-level `haystack pr` CLI command — the original `pr <ref>` /
|
|
5
|
-
* `pr <ref> merge|snooze|dismiss` surface was scope creep on the
|
|
6
|
-
* CLI-as-platform spec. Use the pre-existing `haystack pr-status <pr>`
|
|
7
|
-
* for the same read.
|
|
8
|
-
*
|
|
9
|
-
* Output shape is stamped with `schema_version` per `schemas/pr.v1.json`.
|
|
10
|
-
*/
|
|
11
1
|
import chalk from 'chalk';
|
|
12
2
|
import { loadToken } from '../utils/auth.js';
|
|
13
|
-
import { fetchRatingSynthesis } from '../utils/analysis-api.js';
|
|
3
|
+
import { fetchAutoFixManifest, fetchRatingSynthesis } from '../utils/analysis-api.js';
|
|
14
4
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
5
|
+
import { fetchAnalysisArtifact, fetchGitHubJson, haystackJson } from '../utils/haystack-api.js';
|
|
15
6
|
import { withSchema } from '../schema.js';
|
|
16
|
-
const GITHUB_PROXY = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
|
|
17
7
|
export function parsePrRef(ref) {
|
|
18
8
|
const urlMatch = ref.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
|
|
19
9
|
if (urlMatch)
|
|
20
|
-
return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
|
|
10
|
+
return { owner: urlMatch[1], repo: urlMatch[2], prNumber: Number.parseInt(urlMatch[3], 10) };
|
|
21
11
|
const qualifiedMatch = ref.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
22
12
|
if (qualifiedMatch)
|
|
23
|
-
return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
|
|
13
|
+
return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: Number.parseInt(qualifiedMatch[3], 10) };
|
|
24
14
|
const bare = ref.match(/^#?(\d+)$/);
|
|
25
15
|
if (bare) {
|
|
26
|
-
const
|
|
27
|
-
return { owner:
|
|
16
|
+
const remote = parseRemoteUrl('origin');
|
|
17
|
+
return { owner: remote.owner, repo: remote.repo, prNumber: Number.parseInt(bare[1], 10) };
|
|
28
18
|
}
|
|
29
19
|
throw new Error(`Invalid PR reference: "${ref}". Use 123, owner/repo#123, or a GitHub URL.`);
|
|
30
20
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
export function mergePublicState(meta, entry, queueAvailable = true) {
|
|
22
|
+
if (meta.merged) {
|
|
23
|
+
return { status: 'merged', reason: 'The PR has merged.', blocking_checks: [], action: null };
|
|
24
|
+
}
|
|
25
|
+
if (meta.state === 'closed') {
|
|
26
|
+
return { status: 'closed', reason: 'The PR was closed without merging.', blocking_checks: [], action: null };
|
|
27
|
+
}
|
|
28
|
+
if (!queueAvailable) {
|
|
29
|
+
return {
|
|
30
|
+
status: 'unavailable',
|
|
31
|
+
reason: 'Haystack merge status is temporarily unavailable.',
|
|
32
|
+
blocking_checks: [],
|
|
33
|
+
action: 'Retry this request before making a merge decision.',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!entry) {
|
|
37
|
+
return {
|
|
38
|
+
status: 'not_applicable',
|
|
39
|
+
reason: 'The PR is not currently in Haystack merge processing.',
|
|
40
|
+
blocking_checks: [],
|
|
41
|
+
action: null,
|
|
42
|
+
};
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
const failed = entry.failedChecks ?? [];
|
|
45
|
+
const blocked = failed.length > 0
|
|
46
|
+
|| entry.status === 'failed'
|
|
47
|
+
|| entry.status === 'needs_shepherding'
|
|
48
|
+
|| entry.status === 'awaiting_human_approval';
|
|
49
|
+
return {
|
|
50
|
+
status: blocked ? 'blocked' : 'processing',
|
|
51
|
+
reason: blocked ? 'The PR has merge blockers.' : 'Haystack is processing the PR.',
|
|
52
|
+
blocking_checks: failed,
|
|
53
|
+
action: blocked ? 'Resolve the merge blockers.' : null,
|
|
54
|
+
};
|
|
44
55
|
}
|
|
45
|
-
export async function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.error(chalk.red('Not authenticated. Run `haystack login` first.'));
|
|
50
|
-
process.exit(1);
|
|
56
|
+
export async function fetchMergePublicState(meta, prNumber, owner, repo, token) {
|
|
57
|
+
try {
|
|
58
|
+
const mergeQueue = await haystackJson(`/api/merge-queue/status?repo=${encodeURIComponent(`${owner}/${repo}`)}&branch=${encodeURIComponent(meta.base.ref)}`, token);
|
|
59
|
+
return mergePublicState(meta, mergeQueue.prStatuses?.[String(prNumber)]);
|
|
51
60
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// 404 → null is the "not analyzed yet" signal; we still log unexpected
|
|
55
|
-
// failures so they don't look the same as "not analyzed".
|
|
56
|
-
fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token).catch((err) => {
|
|
57
|
-
console.error(chalk.dim(`[pr] rating fetch failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
58
|
-
return null;
|
|
59
|
-
}),
|
|
60
|
-
]);
|
|
61
|
-
let bucket = 'analyzing';
|
|
62
|
-
if (meta.merged)
|
|
63
|
-
bucket = 'merged';
|
|
64
|
-
else if (meta.state === 'closed')
|
|
65
|
-
bucket = 'merged';
|
|
66
|
-
else if (synthesis?.haystackRating != null) {
|
|
67
|
-
bucket = synthesis.haystackRating <= 2
|
|
68
|
-
? 'issues'
|
|
69
|
-
: synthesis.haystackRating === 5
|
|
70
|
-
? 'good-to-merge'
|
|
71
|
-
: 'needs-attention';
|
|
61
|
+
catch {
|
|
62
|
+
return mergePublicState(meta, undefined, false);
|
|
72
63
|
}
|
|
64
|
+
}
|
|
65
|
+
export async function fetchPrStatePayload(ref) {
|
|
66
|
+
const pr = parsePrRef(ref);
|
|
67
|
+
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
68
|
+
if (!token)
|
|
69
|
+
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
70
|
+
const meta = await fetchGitHubJson(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, token);
|
|
71
|
+
const [synthesis, manifest, traces, merge] = await Promise.all([
|
|
72
|
+
fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
|
|
73
|
+
fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
|
|
74
|
+
fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, 'entire_traces.json', token),
|
|
75
|
+
fetchMergePublicState(meta, pr.prNumber, pr.owner, pr.repo, token),
|
|
76
|
+
]);
|
|
77
|
+
const verdict = synthesis?.analysisVerdict
|
|
78
|
+
?? (synthesis ? (synthesis.haystackRating >= 5 ? 'clean' : 'has-issues') : null);
|
|
73
79
|
const payload = {
|
|
74
|
-
|
|
75
|
-
repo: pr.repo,
|
|
76
|
-
prNumber: pr.prNumber,
|
|
77
|
-
bucket,
|
|
78
|
-
state: meta.merged ? 'merged' : meta.state,
|
|
80
|
+
ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
|
|
79
81
|
title: meta.title,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
mergeQueue: null,
|
|
83
|
-
lastTriage: synthesis?.haystackRating != null
|
|
82
|
+
head_sha: meta.head.sha,
|
|
83
|
+
triage: synthesis
|
|
84
84
|
? {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
status: 'complete',
|
|
86
|
+
verdict: verdict === 'clean' ? 'good_to_merge' : verdict === 'needs-review' ? 'needs_review' : 'needs_input',
|
|
87
|
+
findings_count: synthesis.synthesisDisplay?.length ?? 0,
|
|
88
88
|
}
|
|
89
|
-
: null,
|
|
89
|
+
: { status: 'pending', verdict: null, findings_count: 0 },
|
|
90
|
+
merge,
|
|
91
|
+
traces: {
|
|
92
|
+
available: traces?.hasTraces === true,
|
|
93
|
+
count: traces?.traces?.length ?? 0,
|
|
94
|
+
},
|
|
90
95
|
};
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
if (manifest) {
|
|
97
|
+
const fixed = manifest.fixedItems.length;
|
|
98
|
+
const remaining = manifest.leftForReview.length;
|
|
99
|
+
payload.fixer = {
|
|
100
|
+
status: 'completed',
|
|
101
|
+
result_sha: manifest.resultSha ?? meta.head.sha,
|
|
102
|
+
summary: `Fixed ${fixed} finding${fixed === 1 ? '' : 's'}; ${remaining} remain${remaining === 1 ? 's' : ''} for review.`,
|
|
103
|
+
};
|
|
94
104
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
console.log(
|
|
105
|
+
return withSchema('pr', payload);
|
|
106
|
+
}
|
|
107
|
+
export async function prReadCommand(ref, options) {
|
|
108
|
+
const payload = await fetchPrStatePayload(ref);
|
|
109
|
+
if (options.json) {
|
|
110
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
111
|
+
return;
|
|
101
112
|
}
|
|
102
|
-
|
|
113
|
+
const triage = payload.triage;
|
|
114
|
+
const merge = payload.merge;
|
|
115
|
+
console.log(`\n ${chalk.bold(String(payload.ref))} ${chalk.dim(String(payload.title))}`);
|
|
116
|
+
console.log(` ${chalk.dim('triage:')} ${triage.status}${triage.verdict ? ` · ${triage.verdict}` : ''} · ${triage.findings_count} findings`);
|
|
117
|
+
console.log(` ${chalk.dim('merge:')} ${merge.status} · ${merge.reason}`);
|
|
118
|
+
if (merge.action)
|
|
119
|
+
console.log(` ${chalk.dim('next:')} ${merge.action}`);
|
|
120
|
+
console.log('');
|
|
103
121
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review command — Trigger a fresh Haystack analysis for a PR.
|
|
3
|
+
*
|
|
4
|
+
* haystack review [pr] [--no-wait]
|
|
5
|
+
*
|
|
6
|
+
* Haystack runs a full analysis when a PR is opened (or marked ready);
|
|
7
|
+
* later pushes only get a resolution check against the existing findings.
|
|
8
|
+
* This command is the explicit escape hatch: it re-runs full detection on
|
|
9
|
+
* the PR's current head — use it after substantial new commits, or any
|
|
10
|
+
* time you want a fresh second opinion.
|
|
11
|
+
*
|
|
12
|
+
* When called without a PR identifier, reads the last submitted PR from
|
|
13
|
+
* `.haystack/pending-submit.json` (written by `haystack submit`).
|
|
14
|
+
*
|
|
15
|
+
* Accepts a PR identifier in several formats:
|
|
16
|
+
* haystack review # Last submitted PR
|
|
17
|
+
* haystack review 123 # Uses current repo's owner/repo
|
|
18
|
+
* haystack review owner/repo#123 # Fully qualified
|
|
19
|
+
* haystack review https://github.com/owner/repo/pull/123
|
|
20
|
+
*/
|
|
21
|
+
export interface ReviewOptions {
|
|
22
|
+
wait?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function reviewCommand(identifier: string | undefined, options: ReviewOptions): Promise<void>;
|