@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.
Files changed (46) hide show
  1. package/README.md +45 -6
  2. package/dist/assets/skills/map-your-system.md +138 -0
  3. package/dist/commands/ask.d.ts +14 -0
  4. package/dist/commands/ask.js +20 -0
  5. package/dist/commands/inbox.d.ts +65 -0
  6. package/dist/commands/inbox.js +137 -0
  7. package/dist/commands/mcp.js +88 -134
  8. package/dist/commands/pr.d.ts +37 -1
  9. package/dist/commands/pr.js +95 -77
  10. package/dist/commands/review.d.ts +24 -0
  11. package/dist/commands/review.js +193 -0
  12. package/dist/commands/schema-cmd.js +1 -1
  13. package/dist/commands/skills.js +4 -0
  14. package/dist/commands/traces.d.ts +48 -0
  15. package/dist/commands/traces.js +92 -0
  16. package/dist/commands/triage.d.ts +12 -0
  17. package/dist/commands/triage.js +90 -58
  18. package/dist/commands/webhooks.js +3 -6
  19. package/dist/index.js +79 -148
  20. package/dist/schema.d.ts +5 -2
  21. package/dist/schema.js +5 -2
  22. package/dist/utils/analysis-api.d.ts +40 -0
  23. package/dist/utils/analysis-api.js +108 -3
  24. package/dist/utils/haystack-api.d.ts +20 -0
  25. package/dist/utils/haystack-api.js +66 -0
  26. package/package.json +4 -4
  27. package/schemas/ask.v1.json +40 -0
  28. package/schemas/inbox.v1.json +27 -0
  29. package/schemas/pr.v2.json +54 -0
  30. package/schemas/traces.v1.json +53 -0
  31. package/schemas/triage.v2.json +64 -0
  32. package/dist/assets/skills/install-verification.md +0 -67
  33. package/dist/commands/verification.d.ts +0 -21
  34. package/dist/commands/verification.js +0 -291
  35. package/dist/verification/contract.d.ts +0 -240
  36. package/dist/verification/contract.js +0 -115
  37. package/dist/verification/init.d.ts +0 -10
  38. package/dist/verification/init.js +0 -243
  39. package/dist/verification/manifest.d.ts +0 -49
  40. package/dist/verification/manifest.js +0 -186
  41. package/dist/verification/runner.d.ts +0 -20
  42. package/dist/verification/runner.js +0 -102
  43. package/dist/verification/safety.d.ts +0 -10
  44. package/dist/verification/safety.js +0 -252
  45. package/dist/verification/validate.d.ts +0 -12
  46. package/dist/verification/validate.js +0 -219
@@ -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, } from '@modelcontextprotocol/sdk/types.js';
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 { parsePrRef, prReadCommand } from './pr.js';
25
- import { fetchRatingSynthesis, fetchAutoFixManifest } from '../utils/analysis-api.js';
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, withSchema } from '../schema.js';
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: 'triage_pr',
31
- description: 'Fetch the latest triage results for a pull request. Returns rating, findings, ' +
32
- 'and which findings the auto-fixer is already handling. Identical shape to ' +
33
- '`haystack triage <ref> --json`.',
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
- type: 'string',
39
- description: 'PR reference: bare number, owner/repo#number, or GitHub URL.',
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: 'pr_state',
47
- description: 'Show the Haystack state of a single PR: bucket, last triage rating, merge state.',
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: { type: 'string', description: 'PR reference.' },
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 --json command output. ' +
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
- // dist/commands/mcp.js → ../../schemas/<name>.v<major>.json
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 ok = (data) => ({
123
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
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 === 'triage_pr') {
131
- const ref = args?.ref;
132
- if (!ref)
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
- const pr = parsePrRef(ref);
135
- const token = (await loadToken()) ?? undefined;
136
- const [synthesis, manifest] = await Promise.all([
137
- fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
138
- fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
139
- ]);
140
- if (!synthesis)
141
- return fail(`No triage data for ${pr.owner}/${pr.repo}#${pr.prNumber}`);
142
- // Mirror triage --json envelope.
143
- return ok(withSchema('triage', {
144
- owner: pr.owner,
145
- repo: pr.repo,
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 === 'pr_state') {
160
- const ref = args?.ref;
161
- if (!ref)
113
+ if (name === 'traces_list') {
114
+ if (typeof input.ref !== 'string')
162
115
  return fail('ref required');
163
- const captured = await captureStdout(async () => {
164
- await prReadCommand(ref, { json: true });
165
- });
166
- return ok(jsonFromCapture(captured));
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 n = args?.name;
170
- if (!n || !(n in SCHEMA_VERSIONS)) {
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 (err) {
178
- return fail(`tool error: ${err instanceof Error ? err.message : String(err)}`);
134
+ catch (error) {
135
+ return fail(error instanceof Error ? error.message : String(error));
179
136
  }
180
137
  });
181
- const transport = new StdioServerTransport();
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
  }
@@ -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 prReadCommand(ref: string, opts: PrReadOptions): Promise<void>;
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 {};
@@ -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 r = parseRemoteUrl('origin');
27
- return { owner: r.owner, repo: r.repo, prNumber: parseInt(bare[1], 10) };
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
- async function ghPr(token, owner, repo, num) {
32
- const url = `${GITHUB_PROXY}/api/github/repos/${owner}/${repo}/pulls/${num}`;
33
- const res = await fetch(url, {
34
- headers: {
35
- Accept: 'application/vnd.github+json',
36
- Authorization: `Bearer ${token}`,
37
- 'User-Agent': 'Haystack-CLI',
38
- },
39
- });
40
- if (!res.ok) {
41
- throw new Error(`GitHub API ${res.status}: ${(await res.text()).slice(0, 200)}`);
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
- return res.json();
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 prReadCommand(ref, opts) {
46
- const pr = parsePrRef(ref);
47
- const token = await loadToken();
48
- if (!token) {
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
- const [meta, synthesis] = await Promise.all([
53
- ghPr(token, pr.owner, pr.repo, pr.prNumber),
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
- owner: pr.owner,
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
- headSha: meta.head.sha,
81
- haystackUrl: `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`,
82
- mergeQueue: null,
83
- lastTriage: synthesis?.haystackRating != null
82
+ head_sha: meta.head.sha,
83
+ triage: synthesis
84
84
  ? {
85
- rating: synthesis.haystackRating,
86
- completedAt: new Date().toISOString(),
87
- headSha: meta.head.sha,
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 (opts.json) {
92
- console.log(JSON.stringify(withSchema('pr', payload), null, 2));
93
- return;
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
- console.log(`\n ${chalk.bold(`${pr.owner}/${pr.repo}#${pr.prNumber}`)} ${chalk.dim(meta.title)}`);
96
- console.log(` ${chalk.dim('bucket:')} ${bucket}`);
97
- console.log(` ${chalk.dim('state:')} ${payload.state}`);
98
- console.log(` ${chalk.dim('head:')} ${meta.head.sha.slice(0, 7)} (${meta.head.ref})`);
99
- if (payload.lastTriage) {
100
- console.log(` ${chalk.dim('rating:')} ★${payload.lastTriage.rating}`);
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
- console.log(` ${chalk.dim(payload.haystackUrl)}\n`);
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>;