@haystackeditor/cli 0.25.1 → 0.26.0
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 +39 -455
- package/dist/capture/app-config.js +25 -1
- package/dist/commands/capture-brief.js +41 -32
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +53 -7
- package/dist/commands/init.js +7 -2
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/verify.js +68 -13
- package/dist/index.js +27 -923
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +0 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
package/dist/commands/submit.js
DELETED
|
@@ -1,745 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Submit command - Create a PR from current changes
|
|
3
|
-
*
|
|
4
|
-
* Runs pre-PR triage (code review, rules) on gpt-6-astra in parallel,
|
|
5
|
-
* then pushes the current branch and creates a PR.
|
|
6
|
-
*
|
|
7
|
-
* PRs are routed based on flags:
|
|
8
|
-
* - Default: auto-merge queue (analysis runs, auto-merged if approved)
|
|
9
|
-
* - --review: human review required
|
|
10
|
-
* - --force: skip triage checks
|
|
11
|
-
*/
|
|
12
|
-
import chalk from 'chalk';
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
import { findGitRoot, getCurrentBranch, getDefaultBranch, hasUncommittedChanges, parseRemoteUrl, pushBranch, getLastCommitMessage, getCommitMessagesSinceBase, HEADLESS_PUSH_ERROR, } from '../utils/git.js';
|
|
15
|
-
import { createPullRequest, findExistingPR, getRepositoryAccess, requestReviewers, } from '../utils/github-api.js';
|
|
16
|
-
import { runTriage } from '../triage/runner.js';
|
|
17
|
-
import { loadConfig } from '../utils/config.js';
|
|
18
|
-
import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
|
|
19
|
-
import { savePendingSubmit, updatePendingSubmit } from '../utils/pending-state.js';
|
|
20
|
-
import { ensureHaystackLabels, addLabelsToIssue, } from '../utils/github-api.js';
|
|
21
|
-
import { isHeadlessToken, listStoredAccounts, resolveAuthContext, setRepoDefaultAccount, } from '../utils/auth.js';
|
|
22
|
-
import { HaystackApiError } from '../utils/haystack-api.js';
|
|
23
|
-
import { withSchema } from '../schema.js';
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Helpers
|
|
26
|
-
// ============================================================================
|
|
27
|
-
function buildPRBody(commitMessages) {
|
|
28
|
-
const body = commitMessages || 'Submitted via `haystack submit`.';
|
|
29
|
-
return `${body}
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
*Created via [Haystack CLI](https://haystackeditor.com)*`;
|
|
33
|
-
}
|
|
34
|
-
function canCreateSameRepoPullRequest(permissions) {
|
|
35
|
-
return permissions?.admin === true
|
|
36
|
-
|| permissions?.maintain === true
|
|
37
|
-
|| permissions?.push === true;
|
|
38
|
-
}
|
|
39
|
-
function buildSubmitPermissionMessage(activeLogin, owner, repo, detail) {
|
|
40
|
-
const repoName = `${owner}/${repo}`;
|
|
41
|
-
const lines = [
|
|
42
|
-
`Haystack is authenticated as ${activeLogin}, but that account cannot create a same-repo pull request in ${repoName}.`,
|
|
43
|
-
'Use `haystack submit --account <login>` or `haystack auth use <login>` to select a different saved account.',
|
|
44
|
-
];
|
|
45
|
-
if (detail) {
|
|
46
|
-
lines.push(detail);
|
|
47
|
-
}
|
|
48
|
-
lines.push(`If the correct account is not saved yet, run \`haystack login\` with a GitHub account that has collaborator access to ${repoName}, then retry \`haystack submit\`.`);
|
|
49
|
-
return lines.join('\n');
|
|
50
|
-
}
|
|
51
|
-
function formatSubmitPermissionError(error, activeLogin, owner, repo) {
|
|
52
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
-
const lowerMessage = message.toLowerCase();
|
|
54
|
-
const shouldWrap = lowerMessage.includes('must be a collaborator')
|
|
55
|
-
|| lowerMessage.includes('permission denied')
|
|
56
|
-
|| message.startsWith('Not found: getting repository access');
|
|
57
|
-
if (!shouldWrap) {
|
|
58
|
-
return message;
|
|
59
|
-
}
|
|
60
|
-
return buildSubmitPermissionMessage(activeLogin, owner, repo, `GitHub said: ${message}`);
|
|
61
|
-
}
|
|
62
|
-
async function checkSubmitAccess(owner, repo, authContext) {
|
|
63
|
-
try {
|
|
64
|
-
const repoAccess = await getRepositoryAccess(owner, repo, authContext.token);
|
|
65
|
-
return { repoAccess };
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
return { error };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function ambiguousAccountMessage(owner, repo, logins) {
|
|
72
|
-
return [
|
|
73
|
-
`Multiple saved Haystack accounts can create a same-repo pull request in ${owner}/${repo}: ${logins.join(', ')}.`,
|
|
74
|
-
'Re-run `haystack submit --account <login>` or `haystack auth use <login>` to choose one.',
|
|
75
|
-
].join('\n');
|
|
76
|
-
}
|
|
77
|
-
async function resolveSubmitAuthContext(owner, repo, preferredLogin) {
|
|
78
|
-
const primaryContext = await resolveAuthContext({ preferredLogin, owner, repo });
|
|
79
|
-
const primaryResult = await checkSubmitAccess(owner, repo, primaryContext);
|
|
80
|
-
if (primaryResult.repoAccess && canCreateSameRepoPullRequest(primaryResult.repoAccess.permissions)) {
|
|
81
|
-
return {
|
|
82
|
-
authContext: primaryContext,
|
|
83
|
-
repoAccess: primaryResult.repoAccess,
|
|
84
|
-
autoSelected: false,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (!preferredLogin) {
|
|
88
|
-
const alternatives = [];
|
|
89
|
-
const accounts = await listStoredAccounts();
|
|
90
|
-
for (const account of accounts) {
|
|
91
|
-
if (account.login === primaryContext.login)
|
|
92
|
-
continue;
|
|
93
|
-
const candidateContext = await resolveAuthContext({ preferredLogin: account.login });
|
|
94
|
-
const candidateResult = await checkSubmitAccess(owner, repo, candidateContext);
|
|
95
|
-
if (candidateResult.repoAccess && canCreateSameRepoPullRequest(candidateResult.repoAccess.permissions)) {
|
|
96
|
-
alternatives.push({
|
|
97
|
-
authContext: candidateContext,
|
|
98
|
-
repoAccess: candidateResult.repoAccess,
|
|
99
|
-
autoSelected: true,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (alternatives.length === 1) {
|
|
104
|
-
return alternatives[0];
|
|
105
|
-
}
|
|
106
|
-
if (alternatives.length > 1) {
|
|
107
|
-
throw new Error(ambiguousAccountMessage(owner, repo, alternatives.map((candidate) => candidate.authContext.login)));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (primaryResult.error) {
|
|
111
|
-
throw new Error(formatSubmitPermissionError(primaryResult.error, primaryContext.login, owner, repo));
|
|
112
|
-
}
|
|
113
|
-
throw new Error(buildSubmitPermissionMessage(primaryContext.login, owner, repo, 'The selected Haystack account does not have the collaborator access required for same-repo PR creation.'));
|
|
114
|
-
}
|
|
115
|
-
// ============================================================================
|
|
116
|
-
// Triage output
|
|
117
|
-
// ============================================================================
|
|
118
|
-
function severityIcon(severity) {
|
|
119
|
-
switch (severity) {
|
|
120
|
-
case 'error': return chalk.red('✗');
|
|
121
|
-
case 'warning': return chalk.yellow('!');
|
|
122
|
-
case 'info': return chalk.blue('i');
|
|
123
|
-
default: return chalk.dim('-');
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function severityColor(severity) {
|
|
127
|
-
switch (severity) {
|
|
128
|
-
case 'error': return chalk.red;
|
|
129
|
-
case 'warning': return chalk.yellow;
|
|
130
|
-
case 'info': return chalk.blue;
|
|
131
|
-
default: return chalk.dim;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function checkerDisplayName(checker) {
|
|
135
|
-
switch (checker) {
|
|
136
|
-
case 'code-review': return 'Code Review';
|
|
137
|
-
case 'rules-validator': return 'Rules Validator';
|
|
138
|
-
default: return checker;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Merge triage-tuning knobs from two sources, in ascending precedence:
|
|
143
|
-
* defaults (in runner.ts) < .haystack.json triage.* < CLI flags
|
|
144
|
-
*/
|
|
145
|
-
async function resolveTriageOptions(options) {
|
|
146
|
-
let config = null;
|
|
147
|
-
try {
|
|
148
|
-
config = await loadConfig();
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
// loadConfig returns null when the file doesn't exist — it only throws on
|
|
152
|
-
// read/parse failures, which we must surface so a malformed config doesn't
|
|
153
|
-
// silently lose the user's project-level triage overrides.
|
|
154
|
-
console.warn(chalk.yellow(`⚠ Could not load .haystack.json: ${err.message}`));
|
|
155
|
-
console.warn(chalk.dim(' Triage will use CLI flags / defaults — fix the config to apply project overrides.'));
|
|
156
|
-
}
|
|
157
|
-
const triageConfig = config?.triage ?? {};
|
|
158
|
-
// loadConfig does a raw `as HaystackConfig` cast (no Zod runtime parse), so
|
|
159
|
-
// malformed .haystack.json values can reach us as strings/negatives/NaN.
|
|
160
|
-
// Guard before letting this flow into setTimeout.
|
|
161
|
-
const configTimeoutMs = typeof triageConfig.timeoutMs === 'number' && triageConfig.timeoutMs > 0
|
|
162
|
-
? triageConfig.timeoutMs
|
|
163
|
-
: undefined;
|
|
164
|
-
const timeoutMs = typeof options.triageTimeout === 'number' && options.triageTimeout > 0
|
|
165
|
-
? options.triageTimeout * 1000
|
|
166
|
-
: configTimeoutMs;
|
|
167
|
-
return { timeoutMs };
|
|
168
|
-
}
|
|
169
|
-
function printTriageSummary(result) {
|
|
170
|
-
console.log(chalk.bold('\n' + '━'.repeat(50)));
|
|
171
|
-
console.log(chalk.bold(' Pre-PR Triage Results'));
|
|
172
|
-
console.log(chalk.bold('━'.repeat(50) + '\n'));
|
|
173
|
-
// Per-checker summaries
|
|
174
|
-
for (const checkerResult of result.results) {
|
|
175
|
-
const icon = checkerResult.passed ? chalk.green('✓') : chalk.red('✗');
|
|
176
|
-
const name = checkerDisplayName(checkerResult.checker);
|
|
177
|
-
console.log(` ${icon} ${chalk.bold(name)}: ${checkerResult.summary}`);
|
|
178
|
-
// List issues
|
|
179
|
-
if (checkerResult.issues.length > 0) {
|
|
180
|
-
for (const issue of checkerResult.issues) {
|
|
181
|
-
const location = issue.line
|
|
182
|
-
? `${issue.file}:${issue.line}`
|
|
183
|
-
: issue.file;
|
|
184
|
-
const ruleTag = issue.rule ? chalk.dim(` [${issue.rule}]`) : '';
|
|
185
|
-
const patternTag = issue.pattern ? chalk.dim(` (${issue.pattern})`) : '';
|
|
186
|
-
const categoryTag = issue.category ? chalk.dim(` {${issue.category}}`) : '';
|
|
187
|
-
console.log(` ${severityIcon(issue.severity)} ${chalk.dim(location)} ${severityColor(issue.severity)(issue.message)}${ruleTag}${patternTag}${categoryTag}`);
|
|
188
|
-
if (issue.failureScenario) {
|
|
189
|
-
console.log(chalk.dim(` ${issue.failureScenario}`));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
console.log('');
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
// Checkers that could not run (API error, missing key, stall). Non-blocking.
|
|
196
|
-
for (const err of result.errors) {
|
|
197
|
-
console.log(chalk.yellow(` ! ${err}`));
|
|
198
|
-
}
|
|
199
|
-
if (result.errors.length > 0)
|
|
200
|
-
console.log('');
|
|
201
|
-
// Total summary
|
|
202
|
-
const totalIssues = result.results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
203
|
-
const errorCount = result.results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === 'error').length, 0);
|
|
204
|
-
const warningCount = result.results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === 'warning').length, 0);
|
|
205
|
-
const duration = (result.duration / 1000).toFixed(1);
|
|
206
|
-
if (result.passed && result.results.length > 0) {
|
|
207
|
-
console.log(chalk.green(` All checks passed (${duration}s)\n`));
|
|
208
|
-
}
|
|
209
|
-
else if (result.passed) {
|
|
210
|
-
// No blocking issues, but nothing actually completed either — the
|
|
211
|
-
// triage-unavailable warning will print separately.
|
|
212
|
-
console.log(chalk.yellow(` No checkers produced results (${duration}s)\n`));
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
const parts = [];
|
|
216
|
-
if (errorCount > 0)
|
|
217
|
-
parts.push(chalk.red(`${errorCount} error(s)`));
|
|
218
|
-
if (warningCount > 0)
|
|
219
|
-
parts.push(chalk.yellow(`${warningCount} warning(s)`));
|
|
220
|
-
console.log(` ${parts.join(', ')} found across ${totalIssues} issue(s) (${duration}s)\n`);
|
|
221
|
-
}
|
|
222
|
-
// Results saved location
|
|
223
|
-
console.log(chalk.dim(' Results saved to .haystack/triage/\n'));
|
|
224
|
-
}
|
|
225
|
-
// ============================================================================
|
|
226
|
-
// Main command
|
|
227
|
-
// ============================================================================
|
|
228
|
-
export async function submitCommand(options) {
|
|
229
|
-
// --json contract: stdout carries exactly one JSON document (the submit
|
|
230
|
-
// payload or an error envelope); every progress/status line goes to stderr.
|
|
231
|
-
// Redirecting console.log wholesale keeps the existing progress output
|
|
232
|
-
// intact for humans watching stderr. (console.error/warn already hit stderr.)
|
|
233
|
-
if (options.json) {
|
|
234
|
-
console.log = (...args) => {
|
|
235
|
-
process.stderr.write(args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ') + '\n');
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
/** Print a classified failure (JSON envelope on stdout when --json) and exit 1. */
|
|
239
|
-
const fail = (message, extra) => {
|
|
240
|
-
if (options.json) {
|
|
241
|
-
process.stdout.write(JSON.stringify(withSchema('submit', { status: 'error', error: message, ...extra }), null, 2) + '\n');
|
|
242
|
-
console.error(chalk.red(`\n${message}\n`));
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
console.error(chalk.red(`\n${message}\n`));
|
|
246
|
-
}
|
|
247
|
-
process.exit(1);
|
|
248
|
-
};
|
|
249
|
-
console.log(chalk.bold.cyan('\nHaystack Submit\n'));
|
|
250
|
-
// -------------------------------------------------------------------------
|
|
251
|
-
// Step 1: Validate environment
|
|
252
|
-
// -------------------------------------------------------------------------
|
|
253
|
-
console.log(chalk.dim('Checking environment...'));
|
|
254
|
-
// Validate mutually exclusive options
|
|
255
|
-
if (options.body && options.bodyFile) {
|
|
256
|
-
fail('Cannot use both --body and --body-file. Pick one.');
|
|
257
|
-
}
|
|
258
|
-
// Check git repo
|
|
259
|
-
const gitRoot = findGitRoot();
|
|
260
|
-
if (!gitRoot) {
|
|
261
|
-
fail('Not a git repository. Run `haystack submit` from inside a repository checkout.');
|
|
262
|
-
return; // unreachable — satisfies TS narrowing
|
|
263
|
-
}
|
|
264
|
-
// Get remote info
|
|
265
|
-
let remoteInfo;
|
|
266
|
-
try {
|
|
267
|
-
remoteInfo = parseRemoteUrl('origin');
|
|
268
|
-
console.log(chalk.green(`✓ Repository: ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
fail(err.message);
|
|
272
|
-
return; // unreachable — satisfies TS narrowing
|
|
273
|
-
}
|
|
274
|
-
let githubToken;
|
|
275
|
-
let activeGitHubLogin;
|
|
276
|
-
let autoSelectedAccount = false;
|
|
277
|
-
try {
|
|
278
|
-
const authResolution = await resolveSubmitAuthContext(remoteInfo.owner, remoteInfo.repo, options.account);
|
|
279
|
-
githubToken = authResolution.authContext.token;
|
|
280
|
-
activeGitHubLogin = authResolution.authContext.login;
|
|
281
|
-
autoSelectedAccount = authResolution.autoSelected;
|
|
282
|
-
try {
|
|
283
|
-
await setRepoDefaultAccount(remoteInfo.owner, remoteInfo.repo, activeGitHubLogin);
|
|
284
|
-
}
|
|
285
|
-
catch (err) {
|
|
286
|
-
console.log(chalk.yellow(`⚠ Could not save ${activeGitHubLogin} as the default Haystack account for ${remoteInfo.owner}/${remoteInfo.repo}: ${err.message}`));
|
|
287
|
-
}
|
|
288
|
-
console.log(chalk.green(`✓ Authenticated as ${activeGitHubLogin}`));
|
|
289
|
-
if (autoSelectedAccount) {
|
|
290
|
-
console.log(chalk.dim(` Auto-selected saved account ${activeGitHubLogin} for ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
catch (err) {
|
|
294
|
-
fail(err.message);
|
|
295
|
-
}
|
|
296
|
-
// A headless (hsk_live_) token cannot authenticate a git push, and must not
|
|
297
|
-
// be sent to github.com as a password. Stop before triage and the push.
|
|
298
|
-
if (githubToken && isHeadlessToken(githubToken)) {
|
|
299
|
-
fail(HEADLESS_PUSH_ERROR);
|
|
300
|
-
}
|
|
301
|
-
// Check for uncommitted changes
|
|
302
|
-
if (hasUncommittedChanges()) {
|
|
303
|
-
console.log(chalk.yellow('⚠ You have uncommitted changes. They will not be included in the PR.'));
|
|
304
|
-
}
|
|
305
|
-
// Get current branch info
|
|
306
|
-
const currentBranch = getCurrentBranch();
|
|
307
|
-
const baseBranch = options.base || getDefaultBranch();
|
|
308
|
-
console.log(chalk.dim(` Current branch: ${currentBranch}`));
|
|
309
|
-
console.log(chalk.dim(` Base branch: ${baseBranch}${options.base ? '' : ' (detected from origin)'}`));
|
|
310
|
-
// -------------------------------------------------------------------------
|
|
311
|
-
// Step 2: Pre-PR Triage
|
|
312
|
-
// -------------------------------------------------------------------------
|
|
313
|
-
// Recorded for the --json payload: what triage concluded before the PR
|
|
314
|
-
// was created. 'unavailable' = infrastructure gave zero signal (never
|
|
315
|
-
// reported as 'passed').
|
|
316
|
-
let triageStatus = 'skipped';
|
|
317
|
-
if (!options.force) {
|
|
318
|
-
console.log(chalk.dim('\nRunning pre-PR triage checks...'));
|
|
319
|
-
try {
|
|
320
|
-
const triageOptions = await resolveTriageOptions(options);
|
|
321
|
-
const triageResult = await runTriage(gitRoot, baseBranch, triageOptions);
|
|
322
|
-
printTriageSummary(triageResult);
|
|
323
|
-
// No valid results means we got zero signal from triage: every
|
|
324
|
-
// checker failed to run. Don't pretend it passed — warn and continue.
|
|
325
|
-
const triageInfraFailure = triageResult.results.length === 0 && triageResult.errors.length > 0;
|
|
326
|
-
if (triageInfraFailure) {
|
|
327
|
-
console.log(chalk.yellow('\n⚠ Triage unavailable (no checkers produced results). Continuing with submit.\n'));
|
|
328
|
-
}
|
|
329
|
-
// Only findings with severity "error" block; a failed checker never does.
|
|
330
|
-
if (!triageResult.passed) {
|
|
331
|
-
fail('Triage failed. Fix the issues above or re-run with --force.', {
|
|
332
|
-
triage: {
|
|
333
|
-
status: 'failed',
|
|
334
|
-
issues: triageResult.results.flatMap((r) => r.issues.map((issue) => ({
|
|
335
|
-
checker: r.checker,
|
|
336
|
-
severity: issue.severity,
|
|
337
|
-
file: issue.file,
|
|
338
|
-
line: issue.line ?? null,
|
|
339
|
-
message: issue.message,
|
|
340
|
-
rule: issue.rule ?? null,
|
|
341
|
-
category: issue.category ?? null,
|
|
342
|
-
failure_scenario: issue.failureScenario ?? null,
|
|
343
|
-
}))),
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
triageStatus = triageInfraFailure ? 'unavailable' : 'passed';
|
|
348
|
-
}
|
|
349
|
-
catch (err) {
|
|
350
|
-
// Triage failure should not block submission
|
|
351
|
-
console.log(chalk.yellow(`\n⚠ Triage skipped: ${err.message}`));
|
|
352
|
-
triageStatus = 'unavailable';
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
console.log(chalk.dim('\nSkipping triage (--force)'));
|
|
357
|
-
}
|
|
358
|
-
// -------------------------------------------------------------------------
|
|
359
|
-
// Step 3: Check if on main/master
|
|
360
|
-
// -------------------------------------------------------------------------
|
|
361
|
-
if (currentBranch === 'main' || currentBranch === 'master') {
|
|
362
|
-
fail(`Refusing to submit from ${currentBranch}. Create a feature branch first (e.g. \`git checkout -b my-change\`), then re-run \`haystack submit\`.`);
|
|
363
|
-
}
|
|
364
|
-
// -------------------------------------------------------------------------
|
|
365
|
-
// Step 4: Push to remote
|
|
366
|
-
// -------------------------------------------------------------------------
|
|
367
|
-
console.log(chalk.dim('\nPushing to origin...'));
|
|
368
|
-
if (!githubToken) {
|
|
369
|
-
fail('No Haystack account token was resolved before push (internal error). Run `haystack login`, then retry.');
|
|
370
|
-
return; // unreachable — satisfies TS narrowing
|
|
371
|
-
}
|
|
372
|
-
try {
|
|
373
|
-
pushBranch('origin', currentBranch, { token: githubToken });
|
|
374
|
-
console.log(chalk.green('✓ Branch pushed'));
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
fail(err.message);
|
|
378
|
-
}
|
|
379
|
-
// -------------------------------------------------------------------------
|
|
380
|
-
// Step 5: Create or find PR
|
|
381
|
-
// -------------------------------------------------------------------------
|
|
382
|
-
console.log(chalk.dim('\nCreating pull request...'));
|
|
383
|
-
// Check if PR already exists
|
|
384
|
-
const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch, githubToken);
|
|
385
|
-
let prNumber;
|
|
386
|
-
let prUrl;
|
|
387
|
-
let prTitle;
|
|
388
|
-
// Where the posted title/body came from — echoed to the user and recorded
|
|
389
|
-
// in the --json payload so the derived values are never silent.
|
|
390
|
-
let titleSource;
|
|
391
|
-
let bodySource;
|
|
392
|
-
let prBody = null;
|
|
393
|
-
if (existingPR) {
|
|
394
|
-
console.log(chalk.yellow(`PR already exists: #${existingPR.number}`));
|
|
395
|
-
prNumber = existingPR.number;
|
|
396
|
-
prUrl = existingPR.html_url;
|
|
397
|
-
prTitle = existingPR.title;
|
|
398
|
-
titleSource = 'existing-pr';
|
|
399
|
-
bodySource = 'existing-pr';
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
// Generate PR title and body
|
|
403
|
-
try {
|
|
404
|
-
prTitle = options.title || getLastCommitMessage();
|
|
405
|
-
}
|
|
406
|
-
catch (err) {
|
|
407
|
-
fail(err.message);
|
|
408
|
-
return; // unreachable — satisfies TS narrowing
|
|
409
|
-
}
|
|
410
|
-
titleSource = options.title ? 'flag' : 'last-commit';
|
|
411
|
-
let bodyContent = '';
|
|
412
|
-
if (options.bodyFile) {
|
|
413
|
-
bodySource = options.bodyFile === '-' ? 'stdin' : 'body-file';
|
|
414
|
-
try {
|
|
415
|
-
bodyContent = options.bodyFile === '-'
|
|
416
|
-
? fs.readFileSync(0, 'utf-8')
|
|
417
|
-
: fs.readFileSync(options.bodyFile, 'utf-8');
|
|
418
|
-
}
|
|
419
|
-
catch (err) {
|
|
420
|
-
fail(`Failed to read body file: ${err.message}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
else if (options.body) {
|
|
424
|
-
bodySource = 'flag';
|
|
425
|
-
bodyContent = options.body;
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
bodySource = 'commit-messages';
|
|
429
|
-
bodyContent = getCommitMessagesSinceBase(baseBranch);
|
|
430
|
-
}
|
|
431
|
-
prBody = buildPRBody(bodyContent);
|
|
432
|
-
console.log(chalk.dim(` Title: ${prTitle} (from ${titleSource === 'flag' ? '--title' : 'last commit message'})`));
|
|
433
|
-
console.log(chalk.dim(` Body: from ${bodySource === 'commit-messages' ? 'commit messages since base' : bodySource}`));
|
|
434
|
-
try {
|
|
435
|
-
const pr = await createPullRequest({
|
|
436
|
-
owner: remoteInfo.owner,
|
|
437
|
-
repo: remoteInfo.repo,
|
|
438
|
-
title: prTitle,
|
|
439
|
-
head: currentBranch,
|
|
440
|
-
base: baseBranch,
|
|
441
|
-
body: prBody,
|
|
442
|
-
draft: options.draft,
|
|
443
|
-
token: githubToken,
|
|
444
|
-
});
|
|
445
|
-
prNumber = pr.number;
|
|
446
|
-
prUrl = pr.html_url;
|
|
447
|
-
console.log(chalk.green(`✓ Pull request created: #${prNumber}`));
|
|
448
|
-
}
|
|
449
|
-
catch (err) {
|
|
450
|
-
fail(formatSubmitPermissionError(err, activeGitHubLogin, remoteInfo.owner, remoteInfo.repo));
|
|
451
|
-
return; // unreachable — satisfies TS narrowing
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
// -------------------------------------------------------------------------
|
|
455
|
-
// Step 6: Apply auto-merge label if enabled
|
|
456
|
-
// -------------------------------------------------------------------------
|
|
457
|
-
// Applied-state tracking: the --json payload must report what actually
|
|
458
|
-
// happened on GitHub, not what was requested — label application can fail
|
|
459
|
-
// (403/422) and only warn.
|
|
460
|
-
let autoMergeApplied = false;
|
|
461
|
-
let autoFixApplied = false;
|
|
462
|
-
let reviewLabelApplied = false;
|
|
463
|
-
let reviewerRequestedOk = null;
|
|
464
|
-
// Announce the resolved value AND its source in both directions — a silent
|
|
465
|
-
// config-driven default is invisible to the caller otherwise.
|
|
466
|
-
const autoMergeSourceLabel = options.autoMergeSource === 'flag' ? '--auto-merge flag' : '.haystack.json';
|
|
467
|
-
if (options.autoMerge) {
|
|
468
|
-
try {
|
|
469
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge'], githubToken);
|
|
470
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge'], githubToken);
|
|
471
|
-
autoMergeApplied = true;
|
|
472
|
-
// With --review, the PR still enters the auto-merge queue but the
|
|
473
|
-
// merge queue's needs-review gate holds it until a human approves
|
|
474
|
-
// (removes haystack:needs-review or marks it reviewed). Don't print a
|
|
475
|
-
// bare "Auto-merge enabled" that reads as if it'll merge unreviewed.
|
|
476
|
-
if (options.review) {
|
|
477
|
-
console.log(chalk.green(`✓ Auto-merge enabled via ${autoMergeSourceLabel} (held until a human approves this PR)`));
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
console.log(chalk.green(`✓ Auto-merge enabled for this PR (via ${autoMergeSourceLabel})`));
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
console.log(chalk.yellow(`⚠ Could not set auto-merge label: ${err.message}`));
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
console.log(chalk.dim(` Auto-merge: disabled (via ${autoMergeSourceLabel})`));
|
|
489
|
-
}
|
|
490
|
-
// -------------------------------------------------------------------------
|
|
491
|
-
// Step 6b: Apply auto-fix label if --auto-fix
|
|
492
|
-
// -------------------------------------------------------------------------
|
|
493
|
-
if (options.autoFix) {
|
|
494
|
-
try {
|
|
495
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix'], githubToken);
|
|
496
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix'], githubToken);
|
|
497
|
-
autoFixApplied = true;
|
|
498
|
-
console.log(chalk.green(`✓ Auto-fix (alpha) enabled for this PR (via ${options.autoFixSource === 'flag' ? '--auto-fix flag' : '.haystack.json'})`));
|
|
499
|
-
}
|
|
500
|
-
catch (err) {
|
|
501
|
-
console.log(chalk.yellow(`⚠ Could not set auto-fix label: ${err.message}`));
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// -------------------------------------------------------------------------
|
|
505
|
-
// Step 7: Request review if --review
|
|
506
|
-
// -------------------------------------------------------------------------
|
|
507
|
-
if (options.review) {
|
|
508
|
-
try {
|
|
509
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review'], githubToken);
|
|
510
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review'], githubToken);
|
|
511
|
-
reviewLabelApplied = true;
|
|
512
|
-
if (typeof options.review === 'string') {
|
|
513
|
-
// Specific reviewer requested
|
|
514
|
-
await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review], githubToken);
|
|
515
|
-
reviewerRequestedOk = options.review;
|
|
516
|
-
console.log(chalk.green(`✓ Review requested from ${options.review}`));
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
// No reviewer specified — goes to needs-assignment queue
|
|
520
|
-
console.log(chalk.green('✓ Marked for review (needs assignment)'));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
catch (err) {
|
|
524
|
-
console.log(chalk.yellow(`⚠ Could not request review: ${err.message}`));
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
// -------------------------------------------------------------------------
|
|
528
|
-
// Step 8: Success!
|
|
529
|
-
// -------------------------------------------------------------------------
|
|
530
|
-
console.log(chalk.green('\n' + '━'.repeat(50)));
|
|
531
|
-
console.log(chalk.bold.green(existingPR ? '\nPull Request Updated!\n' : '\nPull Request Created!\n'));
|
|
532
|
-
console.log(` ${chalk.dim('URL:')} ${chalk.cyan(prUrl)}`);
|
|
533
|
-
console.log(` ${chalk.dim('Title:')} ${prTitle}`);
|
|
534
|
-
console.log(` ${chalk.dim('Branch:')} ${currentBranch} → ${baseBranch}`);
|
|
535
|
-
if (options.autoMerge) {
|
|
536
|
-
// --review and auto-merge are not contradictory: the PR is queued but the
|
|
537
|
-
// merge queue's needs-review gate blocks the merge until a human approves.
|
|
538
|
-
// Report it that way instead of a flat "Auto-merge if safe".
|
|
539
|
-
if (options.review) {
|
|
540
|
-
console.log(` ${chalk.dim('Merge:')} ${chalk.yellow('Held for human review, then auto-merge')}`);
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
console.log(` ${chalk.dim('Merge:')} ${chalk.green('Auto-merge if safe')}`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
if (options.autoFix) {
|
|
547
|
-
console.log(` ${chalk.dim('Fix:')} ${chalk.cyan('Auto-fix alpha enabled')}`);
|
|
548
|
-
}
|
|
549
|
-
if (options.review) {
|
|
550
|
-
const reviewer = typeof options.review === 'string' ? options.review : 'needs assignment';
|
|
551
|
-
console.log(` ${chalk.dim('Review:')} ${chalk.yellow(reviewer)}`);
|
|
552
|
-
}
|
|
553
|
-
console.log('');
|
|
554
|
-
// -------------------------------------------------------------------------
|
|
555
|
-
// Step 9: Wait for Haystack analysis (triggered via GitHub App webhook)
|
|
556
|
-
// -------------------------------------------------------------------------
|
|
557
|
-
console.log(chalk.dim('Haystack analysis triggered via GitHub App webhook.'));
|
|
558
|
-
// Save pending state (so `haystack triage` works even if we disconnect)
|
|
559
|
-
const pendingState = {
|
|
560
|
-
version: 1,
|
|
561
|
-
prNumber,
|
|
562
|
-
prUrl,
|
|
563
|
-
owner: remoteInfo.owner,
|
|
564
|
-
repo: remoteInfo.repo,
|
|
565
|
-
branch: currentBranch,
|
|
566
|
-
baseBranch,
|
|
567
|
-
submittedAt: new Date().toISOString(),
|
|
568
|
-
status: 'polling',
|
|
569
|
-
};
|
|
570
|
-
savePendingSubmit(pendingState, gitRoot);
|
|
571
|
-
const ref = `${remoteInfo.owner}/${remoteInfo.repo}#${prNumber}`;
|
|
572
|
-
/** Emit the final --json payload (stdout) — the single machine-readable
|
|
573
|
-
* document of the run. `next` always tells the agent the follow-up command. */
|
|
574
|
-
const emitPayload = (analysis) => {
|
|
575
|
-
if (!options.json)
|
|
576
|
-
return;
|
|
577
|
-
process.stdout.write(JSON.stringify(withSchema('submit', {
|
|
578
|
-
status: existingPR ? 'already_exists' : 'created',
|
|
579
|
-
ref,
|
|
580
|
-
pr_number: prNumber,
|
|
581
|
-
url: prUrl,
|
|
582
|
-
title: prTitle,
|
|
583
|
-
title_source: titleSource,
|
|
584
|
-
body: prBody,
|
|
585
|
-
body_source: bodySource,
|
|
586
|
-
branch: currentBranch,
|
|
587
|
-
base: baseBranch,
|
|
588
|
-
draft: options.draft ?? false,
|
|
589
|
-
account: activeGitHubLogin ?? null,
|
|
590
|
-
// Applied state, not requested state: if a label failed to apply, the
|
|
591
|
-
// payload says so (enabled: false) even though it was asked for.
|
|
592
|
-
auto_merge: { enabled: autoMergeApplied, source: options.autoMergeSource ?? 'config' },
|
|
593
|
-
auto_fix: { enabled: autoFixApplied, source: options.autoFixSource ?? 'config' },
|
|
594
|
-
review_requested: reviewLabelApplied
|
|
595
|
-
? { reviewer: reviewerRequestedOk }
|
|
596
|
-
: null,
|
|
597
|
-
triage: { status: triageStatus },
|
|
598
|
-
analysis,
|
|
599
|
-
next: analysis.status === 'pending' ? `haystack triage ${ref} --json` : null,
|
|
600
|
-
}), null, 2) + '\n');
|
|
601
|
-
};
|
|
602
|
-
// If --no-wait or non-interactive, exit here
|
|
603
|
-
if (options.wait === false || !process.stdin.isTTY) {
|
|
604
|
-
console.log(chalk.dim(`\nAnalysis running in background${options.wait === false ? '' : ' (no TTY — not waiting inline)'}.`));
|
|
605
|
-
console.log(chalk.dim('Check results later with: ') + chalk.cyan(`haystack triage ${ref}\n`));
|
|
606
|
-
emitPayload({ status: 'pending', verdict: null, merged: false, summary: null });
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
// Wait for analysis inline by polling the result endpoint
|
|
610
|
-
console.log(chalk.dim('\nWaiting for analysis...\n'));
|
|
611
|
-
// Handle Ctrl+C gracefully
|
|
612
|
-
let interrupted = false;
|
|
613
|
-
const sigintHandler = () => {
|
|
614
|
-
interrupted = true;
|
|
615
|
-
console.log(chalk.dim('\n\nAnalysis still running. Check later with:'));
|
|
616
|
-
console.log(chalk.cyan(` haystack triage ${ref}\n`));
|
|
617
|
-
emitPayload({ status: 'pending', verdict: null, merged: false, summary: null });
|
|
618
|
-
process.exit(0);
|
|
619
|
-
};
|
|
620
|
-
process.on('SIGINT', sigintHandler);
|
|
621
|
-
const POLL_INTERVAL = 5_000;
|
|
622
|
-
const POLL_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
623
|
-
const deadline = Date.now() + POLL_TIMEOUT;
|
|
624
|
-
let consecutiveErrors = 0;
|
|
625
|
-
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
626
|
-
let emitted = false;
|
|
627
|
-
while (Date.now() < deadline && !interrupted) {
|
|
628
|
-
const result = await checkAnalysisReady(remoteInfo.owner, remoteInfo.repo, prNumber, githubToken);
|
|
629
|
-
if (result.status === 'ready') {
|
|
630
|
-
consecutiveErrors = 0;
|
|
631
|
-
// Fetch results
|
|
632
|
-
try {
|
|
633
|
-
const verdict = await fetchAnalysisResults(remoteInfo.owner, remoteInfo.repo, prNumber, githubToken);
|
|
634
|
-
updatePendingSubmit({
|
|
635
|
-
status: 'completed',
|
|
636
|
-
analysisResult: verdict,
|
|
637
|
-
resolvedAt: new Date().toISOString(),
|
|
638
|
-
}, gitRoot);
|
|
639
|
-
// Print verdict
|
|
640
|
-
console.log(chalk.bold('\n' + '━'.repeat(50)));
|
|
641
|
-
console.log(chalk.bold(' Haystack Analysis Results'));
|
|
642
|
-
console.log(chalk.bold('━'.repeat(50) + '\n'));
|
|
643
|
-
if (verdict.state === 'good-to-merge') {
|
|
644
|
-
// autoMerged here is the GH "merged: true" bit (not the
|
|
645
|
-
// analysis's canAutoMerge eligibility). Only claim the PR was
|
|
646
|
-
// merged when GitHub confirms it.
|
|
647
|
-
if (verdict.autoMerged) {
|
|
648
|
-
console.log(chalk.green.bold(' ✓ Merged\n'));
|
|
649
|
-
console.log(chalk.dim(' No blocking issues. PR is merged on GitHub.'));
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
console.log(chalk.green.bold(' ✓ Good to merge\n'));
|
|
653
|
-
console.log(chalk.dim(` ${verdict.summary}`));
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
else if (verdict.needsHumanReview) {
|
|
657
|
-
// Distinct from generic needs-input: analysis is clean but
|
|
658
|
-
// policy/sensitivity demands a human. Don't lump this in with
|
|
659
|
-
// "issues found" — the user's action is "request a reviewer",
|
|
660
|
-
// not "fix bugs".
|
|
661
|
-
console.log(chalk.cyan.bold(' 👤 Human review required\n'));
|
|
662
|
-
console.log(chalk.dim(` ${verdict.summary}`));
|
|
663
|
-
}
|
|
664
|
-
else if (options.autoFix) {
|
|
665
|
-
// Auto-fix flow: agent fixes straightforward issues in the background.
|
|
666
|
-
// Issues appear in Feed immediately — user can review while agent works.
|
|
667
|
-
console.log(chalk.cyan.bold(' ⚙ Auto-fix triggered (alpha)\n'));
|
|
668
|
-
console.log(chalk.dim(` ${verdict.summary}\n`));
|
|
669
|
-
console.log(chalk.dim(' Coding agent is fixing straightforward mechanical issues in the background.'));
|
|
670
|
-
console.log(chalk.dim(' Auto-fix is currently alpha.'));
|
|
671
|
-
console.log(chalk.dim(' Issues that need your judgment are in the Feed now.\n'));
|
|
672
|
-
const reviewUrl = `https://haystackeditor.com/review/${remoteInfo.owner}/${remoteInfo.repo}/${prNumber}`;
|
|
673
|
-
console.log(` ${chalk.dim('Feed:')} ${chalk.cyan(reviewUrl)}\n`);
|
|
674
|
-
}
|
|
675
|
-
else {
|
|
676
|
-
console.log(chalk.yellow.bold(' ⚠ Needs your input\n'));
|
|
677
|
-
console.log(chalk.dim(` ${verdict.summary}\n`));
|
|
678
|
-
for (const issue of verdict.issues) {
|
|
679
|
-
const icon = issue.severity === 'error' ? chalk.red('✗') : chalk.yellow('!');
|
|
680
|
-
const loc = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
681
|
-
console.log(` ${icon} ${chalk.dim(loc)} ${issue.message}`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
if (!options.autoFix || verdict.state === 'good-to-merge') {
|
|
685
|
-
console.log(`\n ${chalk.dim('Review:')} ${chalk.cyan(verdict.reviewUrl)}\n`);
|
|
686
|
-
}
|
|
687
|
-
emitPayload({
|
|
688
|
-
status: 'complete',
|
|
689
|
-
// Same public vocabulary as `triage --json` / `pr get --json`.
|
|
690
|
-
verdict: verdict.state === 'good-to-merge'
|
|
691
|
-
? 'good_to_merge'
|
|
692
|
-
: verdict.needsHumanReview ? 'needs_review' : 'needs_input',
|
|
693
|
-
merged: verdict.autoMerged ?? false,
|
|
694
|
-
summary: verdict.summary,
|
|
695
|
-
});
|
|
696
|
-
emitted = true;
|
|
697
|
-
}
|
|
698
|
-
catch (err) {
|
|
699
|
-
const message = err.message;
|
|
700
|
-
if (err instanceof HaystackApiError && err.isAuthError) {
|
|
701
|
-
// Auth failure is not transient — retrying `haystack triage` with the
|
|
702
|
-
// same credential would fail identically. Report failed + exit 1.
|
|
703
|
-
console.error(chalk.red(`\n ${message}`));
|
|
704
|
-
emitPayload({ status: 'failed', verdict: null, merged: false, summary: message });
|
|
705
|
-
process.exitCode = 1;
|
|
706
|
-
}
|
|
707
|
-
else {
|
|
708
|
-
// The PR exists and analysis completed — only the result fetch failed.
|
|
709
|
-
console.log(chalk.yellow(`⚠ Analysis complete but could not fetch results: ${message}`));
|
|
710
|
-
console.log(chalk.dim(' Check results with: ') + chalk.cyan(`haystack triage ${ref}\n`));
|
|
711
|
-
emitPayload({ status: 'pending', verdict: null, merged: false, summary: message });
|
|
712
|
-
}
|
|
713
|
-
emitted = true;
|
|
714
|
-
}
|
|
715
|
-
break;
|
|
716
|
-
}
|
|
717
|
-
if (result.status === 'error') {
|
|
718
|
-
consecutiveErrors++;
|
|
719
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
720
|
-
console.log(chalk.red(`\n Analysis check failed: ${result.message}`));
|
|
721
|
-
updatePendingSubmit({ status: 'failed', resolvedAt: new Date().toISOString() }, gitRoot);
|
|
722
|
-
emitPayload({ status: 'failed', verdict: null, merged: false, summary: result.message ?? null });
|
|
723
|
-
emitted = true;
|
|
724
|
-
// The PR was created, but the caller asked us to wait for a verdict
|
|
725
|
-
// and we could not deliver one — that is a failure, not a success.
|
|
726
|
-
process.exitCode = 1;
|
|
727
|
-
break;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
consecutiveErrors = 0;
|
|
732
|
-
}
|
|
733
|
-
process.stderr.write(`\r ${chalk.dim('Status:')} Waiting for analysis...${' '.repeat(10)}`);
|
|
734
|
-
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
735
|
-
}
|
|
736
|
-
// Clean up SIGINT handler
|
|
737
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
738
|
-
if (Date.now() >= deadline && !interrupted) {
|
|
739
|
-
console.log(chalk.yellow('\n\n Timed out waiting for analysis.'));
|
|
740
|
-
console.log(chalk.dim(' Check results later with: ') + chalk.cyan(`haystack triage ${ref}\n`));
|
|
741
|
-
if (!emitted) {
|
|
742
|
-
emitPayload({ status: 'pending', verdict: null, merged: false, summary: 'Timed out waiting for analysis; it continues server-side.' });
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|