@haystackeditor/cli 0.25.1 → 0.27.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.
Files changed (109) hide show
  1. package/README.md +39 -463
  2. package/dist/capture/app-config.js +30 -6
  3. package/dist/commands/capture-brief.js +45 -35
  4. package/dist/commands/capture-contract.js +4 -4
  5. package/dist/commands/crawl-report.js +196 -0
  6. package/dist/commands/db-profile-upload.js +3 -3
  7. package/dist/commands/feedback.js +66 -0
  8. package/dist/commands/init-telemetry.js +55 -9
  9. package/dist/commands/init.js +8 -3
  10. package/dist/commands/lockfile-pin.js +307 -0
  11. package/dist/commands/telemetry-token.js +3 -3
  12. package/dist/commands/tokens.js +4 -4
  13. package/dist/commands/verify-explore.js +3 -3
  14. package/dist/commands/verify-history.js +2 -2
  15. package/dist/commands/verify-onboarding.js +13 -16
  16. package/dist/commands/verify-precompute.js +3 -4
  17. package/dist/commands/verify.js +31 -118
  18. package/dist/index.js +55 -968
  19. package/dist/schema.js +4 -10
  20. package/dist/utils/haystack-api.js +8 -36
  21. package/package.json +1 -5
  22. package/schemas/feedback.v1.json +13 -0
  23. package/schemas/pre-verify.v2.json +240 -0
  24. package/schemas/verify-raw.v1.json +1132 -0
  25. package/schemas/verify.v2.json +655 -0
  26. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  27. package/dist/assets/hooks/agent-context/format.ts +0 -100
  28. package/dist/assets/hooks/agent-context/index.ts +0 -41
  29. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  30. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  31. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  32. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  33. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  34. package/dist/assets/hooks/agent-context/types.ts +0 -58
  35. package/dist/assets/hooks/llm-rules-template.md +0 -59
  36. package/dist/assets/hooks/package-lock.json +0 -598
  37. package/dist/assets/hooks/package.json +0 -12
  38. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  39. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  40. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  41. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  42. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  43. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  44. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  45. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  46. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  47. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  48. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  49. package/dist/assets/skills/map-your-system.md +0 -143
  50. package/dist/assets/skills/submit.md +0 -200
  51. package/dist/commands/ask.js +0 -20
  52. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  53. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  54. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  55. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  56. package/dist/commands/cloud-verifier-materialization.js +0 -704
  57. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  58. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  59. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  60. package/dist/commands/cloud-verifier-universe.js +0 -10178
  61. package/dist/commands/config.js +0 -549
  62. package/dist/commands/design-verify.js +0 -311
  63. package/dist/commands/dismiss.js +0 -159
  64. package/dist/commands/hooks.js +0 -226
  65. package/dist/commands/inbox.js +0 -137
  66. package/dist/commands/mcp.js +0 -201
  67. package/dist/commands/policy.js +0 -371
  68. package/dist/commands/pr-status.js +0 -207
  69. package/dist/commands/pr.js +0 -105
  70. package/dist/commands/prepare-universe-review.js +0 -1092
  71. package/dist/commands/production-source-deny-policy.js +0 -100
  72. package/dist/commands/request-review.js +0 -74
  73. package/dist/commands/review.js +0 -191
  74. package/dist/commands/rules.js +0 -98
  75. package/dist/commands/scaffold-provisional-universe.js +0 -806
  76. package/dist/commands/setup.js +0 -1170
  77. package/dist/commands/skills.js +0 -447
  78. package/dist/commands/status.js +0 -35
  79. package/dist/commands/submit.js +0 -745
  80. package/dist/commands/system-map.js +0 -228
  81. package/dist/commands/triage.js +0 -598
  82. package/dist/commands/webhooks.js +0 -241
  83. package/dist/states.js +0 -46
  84. package/dist/tools/detect.js +0 -832
  85. package/dist/triage/astra.js +0 -202
  86. package/dist/triage/prompts.js +0 -188
  87. package/dist/triage/runner.js +0 -200
  88. package/dist/triage/types.js +0 -7
  89. package/dist/types.js +0 -326
  90. package/dist/utils/action-output.js +0 -26
  91. package/dist/utils/analysis-api.js +0 -416
  92. package/dist/utils/config.js +0 -54
  93. package/dist/utils/design-verifier-api.js +0 -294
  94. package/dist/utils/design-verifier-history.js +0 -79
  95. package/dist/utils/design-verifier-result.js +0 -424
  96. package/dist/utils/github-api.js +0 -324
  97. package/dist/utils/pending-state.js +0 -86
  98. package/dist/utils/pr-ref.js +0 -56
  99. package/dist/utils/prompter.js +0 -328
  100. package/schemas/action.v1.json +0 -22
  101. package/schemas/ask.v1.json +0 -40
  102. package/schemas/inbox.v1.json +0 -27
  103. package/schemas/pr-status.v1.json +0 -61
  104. package/schemas/pr.v1.json +0 -97
  105. package/schemas/pr.v3.json +0 -45
  106. package/schemas/setup.v1.json +0 -75
  107. package/schemas/submit.v1.json +0 -90
  108. package/schemas/triage.v1.json +0 -103
  109. package/schemas/triage.v2.json +0 -64
@@ -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
- }