@haystackeditor/cli 0.15.16 → 0.15.18
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/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/schema-cmd.js +1 -1
- 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 +50 -148
- package/dist/schema.d.ts +5 -2
- package/dist/schema.js +5 -2
- package/dist/triage/prompts.js +4 -0
- package/dist/utils/analysis-api.d.ts +7 -0
- 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/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
|
}
|
|
@@ -37,5 +37,5 @@ export function listSchemas() {
|
|
|
37
37
|
console.log(` ${chalk.cyan(name.padEnd(10))} v${SCHEMA_VERSIONS[name]}`);
|
|
38
38
|
}
|
|
39
39
|
console.log(`\nPrint one with: ${chalk.dim('haystack schema <name>')}\n` +
|
|
40
|
-
`External URL: ${chalk.dim('https://docs.haystackeditor.com/cli/schemas/<name>.
|
|
40
|
+
`External URL: ${chalk.dim('https://docs.haystackeditor.com/cli/schemas/<name>.v<major>.json')}`);
|
|
41
41
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface TraceGetOptions {
|
|
2
|
+
json?: boolean;
|
|
3
|
+
cursor?: string;
|
|
4
|
+
limit?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function fetchTracesListPayload(ref: string): Promise<{
|
|
7
|
+
operation: string;
|
|
8
|
+
ref: string;
|
|
9
|
+
head_sha: string;
|
|
10
|
+
checkpoints: {
|
|
11
|
+
checkpoint_id: string;
|
|
12
|
+
commit_sha: string;
|
|
13
|
+
commit_message: string;
|
|
14
|
+
timestamp: string | null;
|
|
15
|
+
summary: string;
|
|
16
|
+
user_prompt: string | null;
|
|
17
|
+
files_touched: string[];
|
|
18
|
+
branch: string | null;
|
|
19
|
+
}[];
|
|
20
|
+
} & {
|
|
21
|
+
schema_version: string;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function tracesListCommand(ref: string, options: {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
export declare function fetchTracesGetPayload(ref: string, checkpointId: string, options: Pick<TraceGetOptions, 'cursor' | 'limit'>): Promise<{
|
|
27
|
+
operation: string;
|
|
28
|
+
ref: string;
|
|
29
|
+
head_sha: string;
|
|
30
|
+
checkpoint_id: string;
|
|
31
|
+
checkpoint: {
|
|
32
|
+
checkpoint_id: string;
|
|
33
|
+
commit_sha: string;
|
|
34
|
+
commit_message: string;
|
|
35
|
+
timestamp: string | null;
|
|
36
|
+
summary: string;
|
|
37
|
+
user_prompt: string | null;
|
|
38
|
+
files_touched: string[];
|
|
39
|
+
branch: string | null;
|
|
40
|
+
} | null;
|
|
41
|
+
chunks: string[];
|
|
42
|
+
total_chunks: number;
|
|
43
|
+
total_chars: number;
|
|
44
|
+
next_cursor: string | null;
|
|
45
|
+
} & {
|
|
46
|
+
schema_version: string;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function tracesGetCommand(ref: string, checkpointId: string, options: TraceGetOptions): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadToken } from '../utils/auth.js';
|
|
3
|
+
import { fetchAnalysisArtifact, fetchGitHubJson } from '../utils/haystack-api.js';
|
|
4
|
+
import { withSchema } from '../schema.js';
|
|
5
|
+
import { parsePrRef } from './pr.js';
|
|
6
|
+
function publicCheckpoint(trace) {
|
|
7
|
+
return {
|
|
8
|
+
checkpoint_id: trace.checkpointId,
|
|
9
|
+
commit_sha: trace.commitSha,
|
|
10
|
+
commit_message: trace.commitMessage,
|
|
11
|
+
timestamp: trace.timestamp ?? null,
|
|
12
|
+
summary: trace.overviewSummary,
|
|
13
|
+
user_prompt: trace.userPrompt ?? null,
|
|
14
|
+
files_touched: trace.filesTouched ?? [],
|
|
15
|
+
branch: trace.branch ?? null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function tokenAndHead(ref) {
|
|
19
|
+
const pr = parsePrRef(ref);
|
|
20
|
+
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
21
|
+
if (!token)
|
|
22
|
+
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
23
|
+
const meta = await fetchGitHubJson(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, token);
|
|
24
|
+
return { pr, token, headSha: meta.head.sha, ref: `${pr.owner}/${pr.repo}#${pr.prNumber}` };
|
|
25
|
+
}
|
|
26
|
+
export async function fetchTracesListPayload(ref) {
|
|
27
|
+
const { pr, token, headSha, ref: normalizedRef } = await tokenAndHead(ref);
|
|
28
|
+
const overview = await fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, 'entire_traces.json', token);
|
|
29
|
+
return withSchema('traces', {
|
|
30
|
+
operation: 'list',
|
|
31
|
+
ref: normalizedRef,
|
|
32
|
+
head_sha: headSha,
|
|
33
|
+
checkpoints: (overview?.traces ?? []).map(publicCheckpoint),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function tracesListCommand(ref, options) {
|
|
37
|
+
const payload = await fetchTracesListPayload(ref);
|
|
38
|
+
if (options.json) {
|
|
39
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (payload.checkpoints.length === 0) {
|
|
43
|
+
console.log(chalk.dim('\nNo retained customer traces for this PR.\n'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log(chalk.bold('\nCustomer traces\n'));
|
|
47
|
+
for (const trace of payload.checkpoints) {
|
|
48
|
+
console.log(`${chalk.cyan(trace.checkpoint_id)} ${trace.commit_message}`);
|
|
49
|
+
console.log(` ${trace.summary}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function fetchTracesGetPayload(ref, checkpointId, options) {
|
|
53
|
+
const { pr, token, headSha, ref: normalizedRef } = await tokenAndHead(ref);
|
|
54
|
+
const [trace, overview] = await Promise.all([
|
|
55
|
+
fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, `entire_trace_chunks/${encodeURIComponent(checkpointId)}.json`, token),
|
|
56
|
+
fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, 'entire_traces.json', token),
|
|
57
|
+
]);
|
|
58
|
+
if (!trace)
|
|
59
|
+
throw new Error(`Trace checkpoint not found: ${checkpointId}`);
|
|
60
|
+
if (trace.checkpointId !== checkpointId)
|
|
61
|
+
throw new Error('Trace checkpoint provenance mismatch');
|
|
62
|
+
const cursor = Math.max(0, Number.parseInt(options.cursor ?? '0', 10) || 0);
|
|
63
|
+
const limit = Math.min(100, Math.max(1, Number.parseInt(options.limit ?? '20', 10) || 20));
|
|
64
|
+
const chunks = trace.chunks.slice(cursor, cursor + limit);
|
|
65
|
+
const next = cursor + chunks.length < trace.chunks.length ? String(cursor + chunks.length) : null;
|
|
66
|
+
return withSchema('traces', {
|
|
67
|
+
operation: 'get',
|
|
68
|
+
ref: normalizedRef,
|
|
69
|
+
head_sha: headSha,
|
|
70
|
+
checkpoint_id: trace.checkpointId,
|
|
71
|
+
checkpoint: (() => {
|
|
72
|
+
const item = overview?.traces?.find((candidate) => candidate.checkpointId === trace.checkpointId);
|
|
73
|
+
return item ? publicCheckpoint(item) : null;
|
|
74
|
+
})(),
|
|
75
|
+
chunks,
|
|
76
|
+
total_chunks: trace.chunks.length,
|
|
77
|
+
total_chars: trace.totalChars,
|
|
78
|
+
next_cursor: next,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export async function tracesGetCommand(ref, checkpointId, options) {
|
|
82
|
+
const payload = await fetchTracesGetPayload(ref, checkpointId, options);
|
|
83
|
+
const chunks = payload.chunks;
|
|
84
|
+
const next = payload.next_cursor;
|
|
85
|
+
if (options.json) {
|
|
86
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(chunks.join('\n\n---\n\n'));
|
|
90
|
+
if (next)
|
|
91
|
+
console.log(chalk.dim(`\nMore available: --cursor ${next}\n`));
|
|
92
|
+
}
|
|
@@ -19,10 +19,22 @@
|
|
|
19
19
|
* --json Machine-readable JSON
|
|
20
20
|
* (default) Rich terminal output
|
|
21
21
|
*/
|
|
22
|
+
import type { RatingSynthesis, AutoFixManifest } from '../utils/analysis-api.js';
|
|
22
23
|
export interface TriageOptions {
|
|
23
24
|
json?: boolean;
|
|
24
25
|
wait?: boolean;
|
|
25
26
|
hook?: boolean;
|
|
26
27
|
clear?: boolean;
|
|
27
28
|
}
|
|
29
|
+
interface ParsedPR {
|
|
30
|
+
owner: string;
|
|
31
|
+
repo: string;
|
|
32
|
+
prNumber: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function buildJsonOutput(pr: ParsedPR, headSha: string | null, synthesis: RatingSynthesis, manifest?: AutoFixManifest | null): Record<string, unknown>;
|
|
35
|
+
export declare function buildJsonState(pr: ParsedPR, status: 'pending' | 'failed' | 'unavailable', message?: string): Record<string, unknown>;
|
|
36
|
+
export declare function fetchTriageHeadSha(pr: ParsedPR, token?: string): Promise<string | null>;
|
|
28
37
|
export declare function triageCommand(identifier: string | undefined, options: TriageOptions): Promise<void>;
|
|
38
|
+
/** Pure read path for MCP: returns a public payload without writing stdout or exiting. */
|
|
39
|
+
export declare function fetchTriagePayload(identifier: string): Promise<Record<string, unknown>>;
|
|
40
|
+
export {};
|
package/dist/commands/triage.js
CHANGED
|
@@ -26,6 +26,7 @@ import { parseRemoteUrl } from '../utils/git.js';
|
|
|
26
26
|
import { loadToken } from '../utils/auth.js';
|
|
27
27
|
import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
|
|
28
28
|
import { withSchema } from '../schema.js';
|
|
29
|
+
import { fetchGitHubJson } from '../utils/haystack-api.js';
|
|
29
30
|
// ============================================================================
|
|
30
31
|
// PR identifier parsing
|
|
31
32
|
// ============================================================================
|
|
@@ -85,29 +86,14 @@ function isAutoFixing(finding, manifest) {
|
|
|
85
86
|
function printRichOutput(pr, synthesis, manifest) {
|
|
86
87
|
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
87
88
|
console.log(`\n ${chalk.bold(`Haystack Triage: ${pr.owner}/${pr.repo}#${pr.prNumber}`)}\n`);
|
|
88
|
-
//
|
|
89
|
+
// Alpha output stays intentionally small. Findings remain fully visible below.
|
|
89
90
|
if (manifest && (manifest.fixedItems.length > 0 || manifest.leftForReview.length > 0)) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.log(` - [${item.category}] ${item.summary}`);
|
|
94
|
-
}
|
|
95
|
-
console.log(` ${chalk.dim("Don't fix these. The auto-fixer is already on them and will push its own commit.")}\n`);
|
|
96
|
-
}
|
|
97
|
-
if (manifest.leftForReview.length > 0) {
|
|
98
|
-
console.log(chalk.bold(' Auto-fixer skipped these (they need you):\n'));
|
|
99
|
-
for (const item of manifest.leftForReview) {
|
|
100
|
-
console.log(` - [${item.category}] ${item.summary}`);
|
|
101
|
-
console.log(` ${chalk.dim(item.reason)}`);
|
|
102
|
-
}
|
|
103
|
-
console.log('');
|
|
104
|
-
}
|
|
91
|
+
const fixed = manifest.fixedItems.length;
|
|
92
|
+
const remaining = manifest.leftForReview.length;
|
|
93
|
+
console.log(chalk.dim(` Fixer: completed · fixed ${fixed}; ${remaining} remain for review.\n`));
|
|
105
94
|
}
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
const allFindings = synthesis.synthesisDisplay || [];
|
|
109
|
-
const findings = allFindings.filter((f) => !isAutoFixing(f, manifest));
|
|
110
|
-
const fixingCount = manifest?.fixedItems?.length ?? 0;
|
|
95
|
+
// Every customer-facing finding stays visible regardless of fixer state.
|
|
96
|
+
const findings = synthesis.synthesisDisplay || [];
|
|
111
97
|
if (findings.length > 0) {
|
|
112
98
|
console.log(chalk.bold(' Findings to address:\n'));
|
|
113
99
|
for (let i = 0; i < findings.length; i++) {
|
|
@@ -129,35 +115,74 @@ function printRichOutput(pr, synthesis, manifest) {
|
|
|
129
115
|
console.log('');
|
|
130
116
|
}
|
|
131
117
|
}
|
|
132
|
-
else if (fixingCount > 0 && allFindings.length > 0) {
|
|
133
|
-
// Every finding is being auto-fixed — nothing left for the agent.
|
|
134
|
-
console.log(chalk.dim(' Nothing to address. Every finding is being auto-fixed (see above).\n'));
|
|
135
|
-
}
|
|
136
118
|
else {
|
|
137
119
|
console.log(chalk.dim(' No findings.\n'));
|
|
138
120
|
}
|
|
139
121
|
console.log(` ${chalk.dim(reviewUrl)}\n`);
|
|
140
122
|
}
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
123
|
+
export function buildJsonOutput(pr, headSha, synthesis, manifest) {
|
|
124
|
+
const verdict = synthesis.analysisVerdict
|
|
125
|
+
?? (synthesis.haystackRating >= 5 ? 'clean' : 'has-issues');
|
|
126
|
+
const payload = {
|
|
127
|
+
ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
|
|
128
|
+
head_sha: headSha,
|
|
129
|
+
status: 'complete',
|
|
130
|
+
verdict: verdict === 'clean' ? 'good_to_merge' : verdict === 'needs-review' ? 'needs_review' : 'needs_input',
|
|
146
131
|
rating: synthesis.haystackRating,
|
|
147
|
-
|
|
148
|
-
autoFixerHandling: manifest?.fixedItems ?? [],
|
|
149
|
-
autoFixerSkipped: manifest?.leftForReview ?? [],
|
|
132
|
+
human_review_reasons: synthesis.humanReviewReasons ?? [],
|
|
150
133
|
findings: (synthesis.synthesisDisplay || []).map((f) => ({
|
|
134
|
+
id: f.id ?? null,
|
|
151
135
|
category: f.category,
|
|
152
136
|
summary: f.summary,
|
|
153
137
|
detail: f.detail,
|
|
154
|
-
|
|
155
|
-
source: f.source
|
|
156
|
-
|
|
157
|
-
|
|
138
|
+
suggested_fix: f.agentFixPrompt ?? null,
|
|
139
|
+
source: f.source ?? null,
|
|
140
|
+
location: f.filePath || f.startLine != null || f.endLine != null
|
|
141
|
+
? {
|
|
142
|
+
path: f.filePath ?? null,
|
|
143
|
+
start_line: f.startLine ?? null,
|
|
144
|
+
end_line: f.endLine ?? null,
|
|
145
|
+
}
|
|
146
|
+
: null,
|
|
147
|
+
severity: f.severity ?? null,
|
|
158
148
|
})),
|
|
159
|
-
}
|
|
160
|
-
|
|
149
|
+
};
|
|
150
|
+
if (manifest) {
|
|
151
|
+
const fixed = manifest.fixedItems.length;
|
|
152
|
+
const remaining = manifest.leftForReview.length;
|
|
153
|
+
payload.fixer = {
|
|
154
|
+
status: 'completed',
|
|
155
|
+
result_sha: manifest.resultSha ?? headSha,
|
|
156
|
+
summary: `Fixed ${fixed} finding${fixed === 1 ? '' : 's'}; ${remaining} remain${remaining === 1 ? 's' : ''} for review.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return withSchema('triage', payload);
|
|
160
|
+
}
|
|
161
|
+
export function buildJsonState(pr, status, message) {
|
|
162
|
+
const payload = {
|
|
163
|
+
ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
|
|
164
|
+
head_sha: null,
|
|
165
|
+
status,
|
|
166
|
+
verdict: null,
|
|
167
|
+
rating: null,
|
|
168
|
+
human_review_reasons: [],
|
|
169
|
+
findings: [],
|
|
170
|
+
};
|
|
171
|
+
if (message)
|
|
172
|
+
payload.message = message;
|
|
173
|
+
return withSchema('triage', payload);
|
|
174
|
+
}
|
|
175
|
+
export async function fetchTriageHeadSha(pr, token) {
|
|
176
|
+
if (!token)
|
|
177
|
+
return null;
|
|
178
|
+
const meta = await fetchGitHubJson(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, token);
|
|
179
|
+
const headSha = meta.head?.sha;
|
|
180
|
+
if (!headSha)
|
|
181
|
+
throw new Error('GitHub PR metadata response is missing head.sha');
|
|
182
|
+
return headSha;
|
|
183
|
+
}
|
|
184
|
+
function printJsonState(pr, status, message) {
|
|
185
|
+
console.log(JSON.stringify(buildJsonState(pr, status, message), null, 2));
|
|
161
186
|
}
|
|
162
187
|
function ratingStars(rating) {
|
|
163
188
|
const filled = '\u2605'; // ★
|
|
@@ -391,9 +416,7 @@ export async function triageCommand(identifier, options) {
|
|
|
391
416
|
}
|
|
392
417
|
else if (options.wait === false) {
|
|
393
418
|
if (options.json) {
|
|
394
|
-
|
|
395
|
-
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
|
|
396
|
-
}, null, 2));
|
|
419
|
+
printJsonState(pr, 'pending');
|
|
397
420
|
}
|
|
398
421
|
else {
|
|
399
422
|
console.log(chalk.yellow('\n Analysis is still in progress.'));
|
|
@@ -415,9 +438,7 @@ export async function triageCommand(identifier, options) {
|
|
|
415
438
|
}
|
|
416
439
|
else if (waitResult === 'pending') {
|
|
417
440
|
if (options.json) {
|
|
418
|
-
|
|
419
|
-
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
|
|
420
|
-
}, null, 2));
|
|
441
|
+
printJsonState(pr, 'pending', 'Timed out waiting for analysis');
|
|
421
442
|
}
|
|
422
443
|
else {
|
|
423
444
|
console.log(chalk.yellow('\n Timed out waiting for analysis.'));
|
|
@@ -428,9 +449,7 @@ export async function triageCommand(identifier, options) {
|
|
|
428
449
|
else {
|
|
429
450
|
// waitResult === 'error'
|
|
430
451
|
if (options.json) {
|
|
431
|
-
|
|
432
|
-
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'error',
|
|
433
|
-
}, null, 2));
|
|
452
|
+
printJsonState(pr, 'failed', 'Analysis check failed');
|
|
434
453
|
}
|
|
435
454
|
else {
|
|
436
455
|
console.error(chalk.red('\n Analysis check failed.\n'));
|
|
@@ -446,10 +465,7 @@ export async function triageCommand(identifier, options) {
|
|
|
446
465
|
return;
|
|
447
466
|
}
|
|
448
467
|
if (options.json) {
|
|
449
|
-
|
|
450
|
-
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
451
|
-
status: 'error', message: readyResult.message,
|
|
452
|
-
}, null, 2));
|
|
468
|
+
printJsonState(pr, 'failed', readyResult.message);
|
|
453
469
|
}
|
|
454
470
|
else {
|
|
455
471
|
console.error(chalk.red(`\n Analysis check failed: ${readyResult.message}\n`));
|
|
@@ -459,9 +475,10 @@ export async function triageCommand(identifier, options) {
|
|
|
459
475
|
if (!analysisReady)
|
|
460
476
|
return;
|
|
461
477
|
// Fetch the full rating synthesis and auto-fix manifest in parallel
|
|
462
|
-
const [synthesis, autoFixManifest] = await Promise.all([
|
|
478
|
+
const [synthesis, autoFixManifest, headSha] = await Promise.all([
|
|
463
479
|
fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
|
|
464
480
|
fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
|
|
481
|
+
fetchTriageHeadSha(pr, token),
|
|
465
482
|
]);
|
|
466
483
|
// Update pending state if this PR matches the pending submit
|
|
467
484
|
updatePendingSubmitIfExists(pr, synthesis ? 'completed' : undefined);
|
|
@@ -471,10 +488,7 @@ export async function triageCommand(identifier, options) {
|
|
|
471
488
|
return;
|
|
472
489
|
}
|
|
473
490
|
if (options.json) {
|
|
474
|
-
|
|
475
|
-
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
476
|
-
status: 'error', message: 'Rating synthesis not available',
|
|
477
|
-
}, null, 2));
|
|
491
|
+
printJsonState(pr, 'unavailable', 'Rating synthesis not available');
|
|
478
492
|
}
|
|
479
493
|
else {
|
|
480
494
|
console.error(chalk.yellow('\n Analysis completed but rating synthesis is not available yet.'));
|
|
@@ -488,7 +502,7 @@ export async function triageCommand(identifier, options) {
|
|
|
488
502
|
printHookOutput(pr, synthesis, autoFixManifest);
|
|
489
503
|
}
|
|
490
504
|
else if (options.json) {
|
|
491
|
-
|
|
505
|
+
console.log(JSON.stringify(buildJsonOutput(pr, headSha, synthesis, autoFixManifest), null, 2));
|
|
492
506
|
}
|
|
493
507
|
else {
|
|
494
508
|
printRichOutput(pr, synthesis, autoFixManifest);
|
|
@@ -504,6 +518,24 @@ export async function triageCommand(identifier, options) {
|
|
|
504
518
|
});
|
|
505
519
|
}
|
|
506
520
|
}
|
|
521
|
+
/** Pure read path for MCP: returns a public payload without writing stdout or exiting. */
|
|
522
|
+
export async function fetchTriagePayload(identifier) {
|
|
523
|
+
const pr = parsePRIdentifier(identifier);
|
|
524
|
+
const token = await loadToken({ owner: pr.owner, repo: pr.repo }) || undefined;
|
|
525
|
+
const ready = await checkAnalysisReady(pr.owner, pr.repo, pr.prNumber, token);
|
|
526
|
+
if (ready.status === 'pending')
|
|
527
|
+
return buildJsonState(pr, 'pending');
|
|
528
|
+
if (ready.status === 'error')
|
|
529
|
+
return buildJsonState(pr, 'failed', ready.message);
|
|
530
|
+
const [synthesis, manifest, headSha] = await Promise.all([
|
|
531
|
+
fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
|
|
532
|
+
fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
|
|
533
|
+
fetchTriageHeadSha(pr, token),
|
|
534
|
+
]);
|
|
535
|
+
if (!synthesis)
|
|
536
|
+
return buildJsonState(pr, 'unavailable', 'Rating synthesis not available');
|
|
537
|
+
return buildJsonOutput(pr, headSha, synthesis, manifest);
|
|
538
|
+
}
|
|
507
539
|
/**
|
|
508
540
|
* Update the pending-submit state if the given PR matches the pending one.
|
|
509
541
|
* Best-effort — failure is silently ignored.
|
|
@@ -30,14 +30,11 @@ import { parseRemoteUrl, UnsupportedRemoteUrlError } from '../utils/git.js';
|
|
|
30
30
|
import { loadToken } from '../utils/auth.js';
|
|
31
31
|
const DEFAULT_WORKER_URL = process.env.HAYSTACK_WEBHOOKS_URL ??
|
|
32
32
|
'https://haystack-webhooks.akshaysg.workers.dev';
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// lists must agree: a CLI-accepted event with no worker/emitter support
|
|
36
|
-
// either 400s at registration or silently never fires.
|
|
33
|
+
// Public events accepted by the CLI. The backend may support additional
|
|
34
|
+
// gated events, but alpha-only features should not appear in normal CLI help.
|
|
37
35
|
const ALLOWED_EVENTS = [
|
|
38
36
|
'triage.completed',
|
|
39
37
|
'triage.failed',
|
|
40
|
-
'fixer.completed',
|
|
41
38
|
'merge_queue.state_changed',
|
|
42
39
|
// 'review_chat.response_ready' deferred — chat CLI was cut, no producer.
|
|
43
40
|
'pr.classified',
|
|
@@ -124,7 +121,7 @@ async function readJsonOrFail(res) {
|
|
|
124
121
|
// ─── register ────────────────────────────────────────────────────────────────
|
|
125
122
|
export async function registerWebhook(opts) {
|
|
126
123
|
const repo = opts.repo ?? currentRepo();
|
|
127
|
-
const events = opts.events ?? ['triage.completed', 'triage.failed'
|
|
124
|
+
const events = opts.events ?? ['triage.completed', 'triage.failed'];
|
|
128
125
|
for (const e of events) {
|
|
129
126
|
if (!ALLOWED_EVENTS.includes(e)) {
|
|
130
127
|
throw new Error(`Unknown event "${e}". Allowed: ${ALLOWED_EVENTS.join(', ')}`);
|