@haystackeditor/cli 0.25.0 → 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.
Files changed (113) hide show
  1. package/README.md +39 -455
  2. package/dist/capture/adapters/django.js +7 -0
  3. package/dist/capture/adapters/index.js +10 -0
  4. package/dist/capture/adapters/next-build.js +113 -0
  5. package/dist/capture/adapters/next.js +272 -105
  6. package/dist/capture/adapters/nuxt.js +7 -0
  7. package/dist/capture/adapters/rails.js +7 -0
  8. package/dist/capture/adapters/react-router.js +7 -0
  9. package/dist/capture/adapters/sveltekit.js +7 -0
  10. package/dist/capture/adapters/vite.js +14 -0
  11. package/dist/capture/app-config.js +27 -2
  12. package/dist/capture/csp.js +7 -6
  13. package/dist/capture/url-rewrites.js +232 -0
  14. package/dist/capture-step.js +3 -0
  15. package/dist/commands/capture-brief.js +41 -32
  16. package/dist/commands/capture-manifest.js +25 -17
  17. package/dist/commands/feedback.js +66 -0
  18. package/dist/commands/init-capture.js +44 -27
  19. package/dist/commands/init-telemetry.js +77 -14
  20. package/dist/commands/init.js +19 -4
  21. package/dist/commands/lockfile-pin.js +307 -0
  22. package/dist/commands/verify-onboarding.js +16 -0
  23. package/dist/commands/verify.js +70 -15
  24. package/dist/index.js +29 -923
  25. package/dist/schema.js +4 -10
  26. package/dist/utils/haystack-api.js +0 -36
  27. package/package.json +1 -5
  28. package/schemas/feedback.v1.json +13 -0
  29. package/schemas/init.v1.json +1 -1
  30. package/schemas/pre-verify.v2.json +240 -0
  31. package/schemas/verify-raw.v1.json +1132 -0
  32. package/schemas/verify.v2.json +655 -0
  33. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  34. package/dist/assets/hooks/agent-context/format.ts +0 -100
  35. package/dist/assets/hooks/agent-context/index.ts +0 -41
  36. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  37. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  38. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  39. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  40. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  41. package/dist/assets/hooks/agent-context/types.ts +0 -58
  42. package/dist/assets/hooks/llm-rules-template.md +0 -59
  43. package/dist/assets/hooks/package-lock.json +0 -598
  44. package/dist/assets/hooks/package.json +0 -12
  45. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  46. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  47. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  48. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  49. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  50. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  51. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  52. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  53. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  54. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  55. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  56. package/dist/assets/skills/map-your-system.md +0 -143
  57. package/dist/assets/skills/submit.md +0 -200
  58. package/dist/commands/ask.js +0 -20
  59. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  60. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  61. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  62. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  63. package/dist/commands/cloud-verifier-materialization.js +0 -704
  64. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  65. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  66. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  67. package/dist/commands/cloud-verifier-universe.js +0 -10178
  68. package/dist/commands/config.js +0 -549
  69. package/dist/commands/design-verify.js +0 -311
  70. package/dist/commands/dismiss.js +0 -159
  71. package/dist/commands/hooks.js +0 -226
  72. package/dist/commands/inbox.js +0 -137
  73. package/dist/commands/mcp.js +0 -201
  74. package/dist/commands/policy.js +0 -371
  75. package/dist/commands/pr-status.js +0 -207
  76. package/dist/commands/pr.js +0 -105
  77. package/dist/commands/prepare-universe-review.js +0 -1092
  78. package/dist/commands/production-source-deny-policy.js +0 -100
  79. package/dist/commands/request-review.js +0 -74
  80. package/dist/commands/review.js +0 -191
  81. package/dist/commands/rules.js +0 -98
  82. package/dist/commands/scaffold-provisional-universe.js +0 -806
  83. package/dist/commands/setup.js +0 -1170
  84. package/dist/commands/skills.js +0 -447
  85. package/dist/commands/submit.js +0 -745
  86. package/dist/commands/system-map.js +0 -228
  87. package/dist/commands/triage.js +0 -598
  88. package/dist/commands/webhooks.js +0 -241
  89. package/dist/states.js +0 -46
  90. package/dist/tools/detect.js +0 -832
  91. package/dist/triage/astra.js +0 -202
  92. package/dist/triage/prompts.js +0 -188
  93. package/dist/triage/runner.js +0 -200
  94. package/dist/triage/types.js +0 -7
  95. package/dist/utils/action-output.js +0 -26
  96. package/dist/utils/analysis-api.js +0 -416
  97. package/dist/utils/design-verifier-api.js +0 -294
  98. package/dist/utils/design-verifier-history.js +0 -79
  99. package/dist/utils/design-verifier-result.js +0 -424
  100. package/dist/utils/github-api.js +0 -324
  101. package/dist/utils/pending-state.js +0 -86
  102. package/dist/utils/pr-ref.js +0 -56
  103. package/dist/utils/prompter.js +0 -328
  104. package/schemas/action.v1.json +0 -22
  105. package/schemas/ask.v1.json +0 -40
  106. package/schemas/inbox.v1.json +0 -27
  107. package/schemas/pr-status.v1.json +0 -61
  108. package/schemas/pr.v1.json +0 -97
  109. package/schemas/pr.v3.json +0 -45
  110. package/schemas/setup.v1.json +0 -75
  111. package/schemas/submit.v1.json +0 -90
  112. package/schemas/triage.v1.json +0 -103
  113. package/schemas/triage.v2.json +0 -64
@@ -1,371 +0,0 @@
1
- /**
2
- * haystack policy - Manage review policies
3
- *
4
- * Commands:
5
- * list - List all review policies
6
- * add - Add a new policy interactively
7
- * remove - Remove a policy by name
8
- * init - Create initial review-policy.md
9
- */
10
- import chalk from 'chalk';
11
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
12
- import { join, dirname } from 'path';
13
- import { confirm as confirmPrompt, input, select } from '@inquirer/prompts';
14
- import { runPrompt } from '../utils/prompter.js';
15
- const POLICY_PATH = '.haystack/review-policy.md';
16
- // ============================================================================
17
- // Markdown Parsing
18
- // ============================================================================
19
- /**
20
- * Parse review-policy.md content into ReviewPolicy objects and instructions
21
- */
22
- export function parsePolicyMarkdown(content) {
23
- const policies = [];
24
- const instructions = [];
25
- // Split by H2 headings (## )
26
- const sections = content.split(/^## /m).slice(1); // Skip content before first H2
27
- for (const section of sections) {
28
- const lines = section.trim().split('\n');
29
- if (lines.length === 0)
30
- continue;
31
- // First line is the section name
32
- const name = lines[0].trim();
33
- if (!name)
34
- continue;
35
- // ## Instructions — collect bullet points as raw semantic directives
36
- if (name.toLowerCase() === 'instructions') {
37
- for (const line of lines.slice(1)) {
38
- const trimmed = line.trim();
39
- const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
40
- if (bulletMatch) {
41
- instructions.push(bulletMatch[1].trim());
42
- }
43
- }
44
- continue;
45
- }
46
- let paths = [];
47
- let severity = 'medium';
48
- let reason = '';
49
- // Parse fields. serializePolicies (and therefore policy init/add/remove and
50
- // setup) writes them as bullets, `- **Paths**: ...`; hand-written files may
51
- // omit the bullet, so accept both.
52
- for (const line of lines.slice(1)) {
53
- const trimmed = line.trim().replace(/^[-*]\s+/, '');
54
- // Match **Paths**: `pattern1`, `pattern2`
55
- const pathsMatch = trimmed.match(/^\*\*Paths?\*\*:\s*(.+)$/i);
56
- if (pathsMatch) {
57
- const pathString = pathsMatch[1];
58
- const backtickPatterns = pathString.match(/`([^`]+)`/g);
59
- if (backtickPatterns) {
60
- paths = backtickPatterns.map(p => p.replace(/`/g, '').trim());
61
- }
62
- else {
63
- paths = pathString.split(',').map(p => p.trim());
64
- }
65
- continue;
66
- }
67
- // Match **Severity**: critical|high|medium|low
68
- const severityMatch = trimmed.match(/^\*\*Severity\*\*:\s*(critical|high|medium|low)/i);
69
- if (severityMatch) {
70
- severity = severityMatch[1].toLowerCase();
71
- continue;
72
- }
73
- // Match **Reason**: ...
74
- const reasonMatch = trimmed.match(/^\*\*Reason\*\*:\s*(.+)$/i);
75
- if (reasonMatch) {
76
- reason = reasonMatch[1].trim();
77
- continue;
78
- }
79
- }
80
- // Only add if we have at least paths and reason
81
- if (paths.length > 0 && reason) {
82
- policies.push({ name, paths, reason, severity });
83
- }
84
- }
85
- return { policies, instructions };
86
- }
87
- /**
88
- * Serialize ReviewPolicy objects and instructions back to markdown
89
- */
90
- export function serializePolicies(policies, instructions = []) {
91
- const lines = ['# Review Policies', ''];
92
- if (instructions.length > 0) {
93
- lines.push('## Instructions', '');
94
- for (const instruction of instructions) {
95
- lines.push(`- ${instruction}`);
96
- }
97
- lines.push('');
98
- }
99
- for (const policy of policies) {
100
- lines.push(`## ${policy.name}`);
101
- lines.push(`- **Paths**: ${policy.paths.map(p => '`' + p + '`').join(', ')}`);
102
- lines.push(`- **Severity**: ${policy.severity}`);
103
- lines.push(`- **Reason**: ${policy.reason}`);
104
- lines.push('');
105
- }
106
- return lines.join('\n');
107
- }
108
- // ============================================================================
109
- // Severity Colors
110
- // ============================================================================
111
- function severityColor(severity) {
112
- switch (severity) {
113
- case 'critical': return chalk.red;
114
- case 'high': return chalk.yellow;
115
- case 'medium': return chalk.blue;
116
- case 'low': return chalk.dim;
117
- default: return chalk.white;
118
- }
119
- }
120
- // ============================================================================
121
- // Commands
122
- // ============================================================================
123
- /**
124
- * List all review policies
125
- */
126
- export async function listPolicies() {
127
- const filePath = join(process.cwd(), POLICY_PATH);
128
- if (!existsSync(filePath)) {
129
- console.log(chalk.yellow('\nNo review-policy.md found.'));
130
- console.log(chalk.dim('Run "haystack policy init" to create one.\n'));
131
- return;
132
- }
133
- const content = readFileSync(filePath, 'utf-8');
134
- const { policies, instructions } = parsePolicyMarkdown(content);
135
- if (policies.length === 0 && instructions.length === 0) {
136
- console.log(chalk.yellow('\nNo policies or instructions defined in review-policy.md'));
137
- console.log(chalk.dim('Run "haystack policy add" to create one.\n'));
138
- return;
139
- }
140
- if (instructions.length > 0) {
141
- console.log(chalk.cyan('\n📝 Review Instructions\n'));
142
- for (const instruction of instructions) {
143
- console.log(` ${chalk.dim('•')} ${instruction}`);
144
- }
145
- console.log('');
146
- }
147
- if (policies.length > 0) {
148
- console.log(chalk.cyan('📋 Review Policies\n'));
149
- for (const policy of policies) {
150
- const color = severityColor(policy.severity);
151
- console.log(` ${chalk.bold(policy.name)}`);
152
- console.log(` Severity: ${color(policy.severity)}`);
153
- console.log(` Paths: ${chalk.dim(policy.paths.join(', '))}`);
154
- console.log(` Reason: ${policy.reason}`);
155
- console.log('');
156
- }
157
- }
158
- }
159
- /**
160
- * Add a new review policy interactively
161
- */
162
- export async function addPolicy(nameArg) {
163
- const filePath = join(process.cwd(), POLICY_PATH);
164
- // Load existing policies and instructions
165
- let policies = [];
166
- let instructions = [];
167
- if (existsSync(filePath)) {
168
- const existingContent = readFileSync(filePath, 'utf-8');
169
- const parsed = parsePolicyMarkdown(existingContent);
170
- policies = parsed.policies;
171
- instructions = parsed.instructions;
172
- }
173
- // Interactive-only command: without a TTY the inquirer prompt would hang a
174
- // non-interactive caller (e.g. a coding agent) forever.
175
- if (!process.stdin.isTTY) {
176
- console.error(chalk.red('\n`haystack policy add` is interactive and requires a terminal.\n' +
177
- `Non-interactive alternative: edit ${POLICY_PATH} directly ` +
178
- '(run `haystack policy list` to see the current policies and format).\n'));
179
- process.exit(1);
180
- }
181
- console.log(chalk.cyan('\n➕ Add Review Policy\n'));
182
- // Prompt for policy details
183
- const answers = {
184
- name: await runPrompt(input({
185
- message: 'Policy name:',
186
- default: nameArg,
187
- validate: (value) => {
188
- if (!value.trim())
189
- return 'Name is required';
190
- if (policies.some(p => p.name.toLowerCase() === value.toLowerCase())) {
191
- return 'A policy with this name already exists';
192
- }
193
- return true;
194
- },
195
- })),
196
- paths: await runPrompt(input({
197
- message: 'File patterns (comma-separated globs):',
198
- validate: (value) => value.trim() ? true : 'At least one path pattern is required',
199
- })),
200
- severity: await runPrompt(select({
201
- message: 'Severity:',
202
- choices: [
203
- { name: chalk.red('critical') + ' - Always requires human review', value: 'critical' },
204
- { name: chalk.yellow('high') + ' - Usually requires review', value: 'high' },
205
- { name: chalk.blue('medium') + ' - Should be reviewed', value: 'medium' },
206
- { name: chalk.dim('low') + ' - Nice to review', value: 'low' },
207
- ],
208
- default: 'medium',
209
- })),
210
- reason: await runPrompt(input({
211
- message: 'Reason for requiring review:',
212
- validate: (value) => value.trim() ? true : 'Reason is required',
213
- })),
214
- };
215
- // Parse paths from comma-separated input
216
- const paths = answers.paths.split(',').map((p) => p.trim()).filter(Boolean);
217
- const newPolicy = {
218
- name: answers.name.trim(),
219
- paths,
220
- severity: answers.severity,
221
- reason: answers.reason.trim(),
222
- };
223
- policies.push(newPolicy);
224
- // Ensure directory exists
225
- const dir = dirname(filePath);
226
- if (!existsSync(dir)) {
227
- mkdirSync(dir, { recursive: true });
228
- }
229
- // Write updated file
230
- writeFileSync(filePath, serializePolicies(policies, instructions));
231
- console.log(chalk.green(`\n✓ Added policy "${newPolicy.name}"`));
232
- console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
233
- }
234
- /**
235
- * Remove a policy by name
236
- */
237
- export async function removePolicy(name) {
238
- const filePath = join(process.cwd(), POLICY_PATH);
239
- if (!existsSync(filePath)) {
240
- console.log(chalk.red('\nNo review-policy.md found.\n'));
241
- return;
242
- }
243
- const content = readFileSync(filePath, 'utf-8');
244
- const parsed = parsePolicyMarkdown(content);
245
- let policies = parsed.policies;
246
- const instructions = parsed.instructions;
247
- // Find policy (case-insensitive)
248
- const index = policies.findIndex(p => p.name.toLowerCase() === name.toLowerCase());
249
- if (index === -1) {
250
- console.log(chalk.red(`\nPolicy "${name}" not found.\n`));
251
- console.log(chalk.dim('Available policies:'));
252
- for (const p of policies) {
253
- console.log(chalk.dim(` - ${p.name}`));
254
- }
255
- console.log('');
256
- return;
257
- }
258
- const removed = policies[index];
259
- // Confirm removal. Without a TTY the prompt would hang — the command names
260
- // an exact policy, so treat the invocation itself as the confirmation.
261
- if (process.stdin.isTTY) {
262
- const confirm = await runPrompt(confirmPrompt({
263
- message: `Remove policy "${removed.name}"?`,
264
- default: false,
265
- }));
266
- if (!confirm) {
267
- console.log(chalk.dim('\nCancelled.\n'));
268
- return;
269
- }
270
- }
271
- else {
272
- console.error(chalk.dim(`No TTY — removing "${removed.name}" without interactive confirmation.`));
273
- }
274
- // Remove and write
275
- policies = policies.filter((_, i) => i !== index);
276
- writeFileSync(filePath, serializePolicies(policies, instructions));
277
- console.log(chalk.green(`\n✓ Removed policy "${removed.name}"`));
278
- console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
279
- }
280
- /**
281
- * Add a review instruction
282
- */
283
- export async function addInstruction(text) {
284
- const filePath = join(process.cwd(), POLICY_PATH);
285
- // Load existing
286
- let policies = [];
287
- let instructions = [];
288
- if (existsSync(filePath)) {
289
- const content = readFileSync(filePath, 'utf-8');
290
- const parsed = parsePolicyMarkdown(content);
291
- policies = parsed.policies;
292
- instructions = parsed.instructions;
293
- }
294
- let instruction = text?.trim();
295
- if (!instruction) {
296
- if (!process.stdin.isTTY) {
297
- console.error(chalk.red('\nNo instruction text given and no terminal to prompt in.\n' +
298
- 'Pass the text directly: haystack policy add-instruction "Never flag weak test coverage"\n'));
299
- process.exit(1);
300
- }
301
- const answer = await runPrompt(input({
302
- message: 'Review instruction:',
303
- validate: (value) => value.trim() ? true : 'Instruction text is required',
304
- }));
305
- instruction = answer.trim();
306
- }
307
- instructions.push(instruction);
308
- // Ensure directory exists
309
- const dir = dirname(filePath);
310
- if (!existsSync(dir)) {
311
- mkdirSync(dir, { recursive: true });
312
- }
313
- writeFileSync(filePath, serializePolicies(policies, instructions));
314
- console.log(chalk.green(`\n✓ Added instruction: "${instruction}"`));
315
- console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
316
- }
317
- /**
318
- * Create initial review-policy.md with example policies
319
- */
320
- export async function initPolicies(options) {
321
- const filePath = join(process.cwd(), POLICY_PATH);
322
- if (existsSync(filePath) && !options.force) {
323
- console.log(chalk.yellow('\nreview-policy.md already exists. Use --force to overwrite.\n'));
324
- return;
325
- }
326
- // Default template policies
327
- const defaultPolicies = [
328
- {
329
- name: 'Infrastructure changes',
330
- paths: ['terraform/**', '*.tf', 'pulumi/**', 'cdk/**'],
331
- severity: 'high',
332
- reason: 'Infrastructure changes require manual review for security and cost implications',
333
- },
334
- {
335
- name: 'Secret files',
336
- paths: ['**/*.secret*', '**/*.env*', '**/credentials*', '**/*.pem', '**/*.key'],
337
- severity: 'critical',
338
- reason: 'Files potentially containing secrets require security review',
339
- },
340
- {
341
- name: 'CI/CD pipelines',
342
- paths: ['.github/workflows/**', '.gitlab-ci.yml', 'Jenkinsfile', '.circleci/**'],
343
- severity: 'high',
344
- reason: 'CI/CD changes can affect deployment security and reliability',
345
- },
346
- {
347
- name: 'Weak test coverage',
348
- paths: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs'],
349
- severity: 'medium',
350
- reason: 'Non-trivial logic changes without test coverage should be reviewed by a human to verify correctness',
351
- },
352
- {
353
- name: 'Unverified agent changes',
354
- paths: ['**/*'],
355
- severity: 'medium',
356
- reason: 'Agent-authored PRs without verification evidence (tests run, build checked, UI verified) should be reviewed by a human',
357
- },
358
- ];
359
- // Ensure directory exists
360
- const dir = dirname(filePath);
361
- if (!existsSync(dir)) {
362
- mkdirSync(dir, { recursive: true });
363
- }
364
- writeFileSync(filePath, serializePolicies(defaultPolicies));
365
- console.log(chalk.green(`\n✓ Created ${POLICY_PATH}`));
366
- console.log(chalk.dim('\nDefault policies added:'));
367
- for (const p of defaultPolicies) {
368
- console.log(chalk.dim(` - ${p.name} (${p.severity})`));
369
- }
370
- console.log(chalk.dim('\nEdit the file or use "haystack policy add" / "haystack policy add-instruction" to customize.\n'));
371
- }
@@ -1,207 +0,0 @@
1
- /**
2
- * pr-status command — Show the current Haystack status of a PR.
3
- *
4
- * Calls the classify-debug endpoint to determine which inbox bucket the PR
5
- * is in (analyzing, auto_fixing, good_to_merge, issues, needs_assignment,
6
- * etc.) and displays a human-readable summary.
7
- *
8
- * `--json` emits the versioned `pr-status` schema (see `haystack schema
9
- * pr-status`), with all state tokens normalized to the canonical snake_case
10
- * vocabulary shared by every CLI JSON surface. The server's raw kebab-case
11
- * classify-debug response is an internal shape and is not exposed.
12
- *
13
- * Accepts a PR identifier in several formats:
14
- * 123 # Uses current repo's owner/repo
15
- * #123 # PR number with hash
16
- * owner/repo#123 # Fully qualified
17
- * https://github.com/owner/repo/pull/123 # GitHub URL
18
- */
19
- import chalk from 'chalk';
20
- import { resolveAuthContext } from '../utils/auth.js';
21
- import { classifyHttpError } from '../utils/haystack-api.js';
22
- import { parsePrRef } from '../utils/pr-ref.js';
23
- import { normalizeFeedBucket, toPublicVerdict } from '../states.js';
24
- import { withSchema } from '../schema.js';
25
- export function buildPrStatusPayload(pr, data) {
26
- const inputs = data.inputs ?? {};
27
- return withSchema('pr-status', {
28
- ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
29
- bucket: normalizeFeedBucket(data.bucket),
30
- in_feed: data.isInFeed ?? null,
31
- feed_priority: data.feedPriority ?? null,
32
- reason: data.reason,
33
- analysis: {
34
- status: inputs.analysisStatus ?? 'pending',
35
- // Verdict only exists once analysis is ready; no rating-based guessing here.
36
- verdict: inputs.analysisVerdict
37
- ? toPublicVerdict(inputs.analysisVerdict, inputs.haystackRating)
38
- : null,
39
- rating: inputs.haystackRating ?? null,
40
- },
41
- needs_human_review: inputs.needsHumanReview ?? null,
42
- labels: inputs.labels ?? [],
43
- overrides: {
44
- snoozed: inputs.snoozed ?? false,
45
- dismissed: inputs.dismissed ?? false,
46
- dismissed_agent_alerts: inputs.dismissedAgentAlerts ?? false,
47
- dismissed_needs_assignment: inputs.dismissedNeedsAssignment ?? false,
48
- manually_good_to_merge: inputs.manuallyGoodToMerge ?? false,
49
- },
50
- // The classify endpoint can return 200 with missing PR metadata (its own
51
- // GitHub fetch failed) — coalesce to null so required fields never vanish
52
- // from the serialized payload (undefined keys are dropped by JSON.stringify).
53
- author: inputs.author ?? null,
54
- is_author: inputs.isAuthor ?? null,
55
- is_open: inputs.isOpen ?? null,
56
- state_synced_at: data.stateSyncedAt ?? null,
57
- stale_warning: data.staleWarning ?? null,
58
- });
59
- }
60
- // ============================================================================
61
- // Display helpers
62
- // ============================================================================
63
- export const BUCKET_DISPLAY = {
64
- 'analyzing': { label: 'Analyzing', color: chalk.blue, icon: '...' },
65
- 'auto_fixing': { label: 'Auto-fixing (alpha)', color: chalk.blue, icon: '>>>' },
66
- 'good_to_merge': { label: 'Good to Merge', color: chalk.green, icon: '[+]' },
67
- 'failed_when_merging': { label: 'Failed When Merging', color: chalk.red, icon: '[!]' },
68
- 'issues': { label: 'Issues Found', color: chalk.yellow, icon: '[?]' },
69
- 'needs_assignment': { label: 'Needs Reviewer Assignment', color: chalk.yellow, icon: '[?]' },
70
- 'needs_shepherding': { label: 'Needs Manual Merge', color: chalk.yellow, icon: '[!]' },
71
- 'needs_fixing': { label: 'Agent Fixing CI', color: chalk.blue, icon: '>>>' },
72
- 'awaiting_review': { label: 'Awaiting Review', color: chalk.cyan, icon: '[-]' },
73
- 'review_requested': { label: 'Review Requested', color: chalk.cyan, icon: '[-]' },
74
- 'snoozed': { label: 'Snoozed', color: chalk.dim, icon: '[-]' },
75
- };
76
- function formatBucket(bucket) {
77
- if (!bucket)
78
- return chalk.dim('Not classified (PR may be closed, or not yet analyzed)');
79
- const info = BUCKET_DISPLAY[bucket];
80
- if (!info)
81
- return chalk.dim(bucket);
82
- return `${info.icon} ${info.color(info.label)}`;
83
- }
84
- function formatAnalysisStatus(status) {
85
- switch (status) {
86
- case 'ready': return chalk.green('Ready');
87
- case 'in_queue': return chalk.blue('In queue');
88
- case 'pending': return chalk.dim('Pending');
89
- case 'error': return chalk.red('Error');
90
- default: return chalk.dim(status ?? 'unknown');
91
- }
92
- }
93
- function formatVerdict(verdict) {
94
- switch (verdict) {
95
- case 'good_to_merge': return chalk.green('Good to merge');
96
- case 'needs_input': return chalk.yellow('Needs input');
97
- case 'needs_review': return chalk.cyan('Needs review');
98
- default: return chalk.dim('n/a');
99
- }
100
- }
101
- // ============================================================================
102
- // API call
103
- // ============================================================================
104
- const HAYSTACK_API = 'https://haystackeditor.com';
105
- async function fetchClassification(owner, repo, prNumber, token) {
106
- const prId = `${owner}/${repo}#${prNumber}`;
107
- const response = await fetch(`${HAYSTACK_API}/auth/classify-debug?pr=${encodeURIComponent(prId)}`, {
108
- headers: {
109
- Authorization: `Bearer ${token}`,
110
- 'User-Agent': 'Haystack-CLI',
111
- },
112
- });
113
- if (!response.ok) {
114
- throw await classifyHttpError(response, `PR status for ${prId}`);
115
- }
116
- return response.json();
117
- }
118
- /** Pure read path (also used by MCP): returns the versioned payload without writing stdout. */
119
- export async function fetchPrStatusPayload(identifier) {
120
- const pr = parsePrRef(identifier, 'pr-status');
121
- const authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
122
- const data = await fetchClassification(pr.owner, pr.repo, pr.prNumber, authContext.token);
123
- return buildPrStatusPayload(pr, data);
124
- }
125
- // ============================================================================
126
- // Command
127
- // ============================================================================
128
- export async function prStatusCommand(identifier, options) {
129
- let pr;
130
- try {
131
- pr = parsePrRef(identifier, 'pr-status');
132
- }
133
- catch (err) {
134
- console.error(chalk.red(`\n${err.message}\n`));
135
- process.exit(1);
136
- }
137
- let authContext;
138
- try {
139
- authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
140
- }
141
- catch (err) {
142
- console.error(chalk.red(`\n${err.message}\n`));
143
- process.exit(1);
144
- }
145
- const token = authContext.token;
146
- const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
147
- if (!options.json) {
148
- console.log(chalk.dim(`\nFetching status for ${prLabel}...\n`));
149
- }
150
- let data;
151
- try {
152
- data = await fetchClassification(pr.owner, pr.repo, pr.prNumber, token);
153
- }
154
- catch (err) {
155
- console.error(chalk.red(`\n${err.message}\n`));
156
- process.exit(1);
157
- }
158
- const payload = buildPrStatusPayload(pr, data);
159
- // JSON mode — pure JSON to stdout, nothing else
160
- if (options.json) {
161
- console.log(JSON.stringify(payload, null, 2));
162
- return;
163
- }
164
- // Human-readable output
165
- console.log(` ${chalk.bold('PR')}: ${payload.ref}`);
166
- console.log(` ${chalk.bold('Status')}: ${formatBucket(payload.bucket)}`);
167
- console.log(` ${chalk.bold('Analysis')}: ${formatAnalysisStatus(payload.analysis.status)}`);
168
- if (payload.analysis.verdict) {
169
- console.log(` ${chalk.bold('Verdict')}: ${formatVerdict(payload.analysis.verdict)}`);
170
- }
171
- if (payload.analysis.rating != null) {
172
- console.log(` ${chalk.bold('Rating')}: ${payload.analysis.rating}/5`);
173
- }
174
- console.log(` ${chalk.bold('Reason')}: ${chalk.dim(payload.reason)}`);
175
- // Extra context
176
- if (!payload.is_open) {
177
- console.log(chalk.dim(`\n PR is closed or merged.`));
178
- }
179
- if (!payload.is_author) {
180
- console.log(chalk.dim(`\n You are not the author (author: ${payload.author}).`));
181
- }
182
- // Labels of interest
183
- const haystackLabels = payload.labels.filter(l => l.startsWith('haystack:'));
184
- if (haystackLabels.length > 0) {
185
- console.log(` ${chalk.bold('Labels')}: ${haystackLabels.join(', ')}`);
186
- }
187
- // User overrides
188
- const overrides = [];
189
- if (payload.overrides.dismissed)
190
- overrides.push('dismissed');
191
- if (payload.overrides.dismissed_agent_alerts)
192
- overrides.push('agent alerts dismissed');
193
- if (payload.overrides.dismissed_needs_assignment)
194
- overrides.push('needs-assignment dismissed');
195
- if (payload.overrides.manually_good_to_merge)
196
- overrides.push('manually marked good-to-merge');
197
- if (payload.overrides.snoozed)
198
- overrides.push('snoozed');
199
- if (overrides.length > 0) {
200
- console.log(` ${chalk.bold('Overrides')}: ${overrides.join(', ')}`);
201
- }
202
- // Stale warning
203
- if (payload.stale_warning) {
204
- console.log(chalk.yellow(`\n Warning: ${payload.stale_warning}`));
205
- }
206
- console.log('');
207
- }
@@ -1,105 +0,0 @@
1
- import chalk from 'chalk';
2
- import { loadToken } from '../utils/auth.js';
3
- import { fetchAutoFixManifest, fetchRatingSynthesis } from '../utils/analysis-api.js';
4
- import { fetchGitHubJson, haystackJson, HaystackApiError } from '../utils/haystack-api.js';
5
- import { parsePrRef } from '../utils/pr-ref.js';
6
- import { toPublicVerdict } from '../states.js';
7
- import { withSchema } from '../schema.js';
8
- export function mergePublicState(meta, entry, queueAvailable = true) {
9
- if (meta.merged) {
10
- return { status: 'merged', reason: 'The PR has merged.', blocking_checks: [], action: null };
11
- }
12
- if (meta.state === 'closed') {
13
- return { status: 'closed', reason: 'The PR was closed without merging.', blocking_checks: [], action: null };
14
- }
15
- if (!queueAvailable) {
16
- return {
17
- status: 'unavailable',
18
- reason: 'Haystack merge status is temporarily unavailable.',
19
- blocking_checks: [],
20
- action: 'Retry this request before making a merge decision.',
21
- };
22
- }
23
- if (!entry) {
24
- return {
25
- status: 'not_applicable',
26
- reason: 'The PR is not currently in Haystack merge processing.',
27
- blocking_checks: [],
28
- action: null,
29
- };
30
- }
31
- const failed = entry.failedChecks ?? [];
32
- const blocked = failed.length > 0
33
- || entry.status === 'failed'
34
- || entry.status === 'needs_shepherding'
35
- || entry.status === 'awaiting_human_approval';
36
- return {
37
- status: blocked ? 'blocked' : 'processing',
38
- reason: blocked ? 'The PR has merge blockers.' : 'Haystack is processing the PR.',
39
- blocking_checks: failed,
40
- action: blocked ? 'Resolve the merge blockers.' : null,
41
- };
42
- }
43
- export async function fetchMergePublicState(meta, prNumber, owner, repo, token) {
44
- try {
45
- const mergeQueue = await haystackJson(`/api/merge-queue/status?repo=${encodeURIComponent(`${owner}/${repo}`)}&branch=${encodeURIComponent(meta.base.ref)}`, token);
46
- return mergePublicState(meta, mergeQueue.prStatuses?.[String(prNumber)]);
47
- }
48
- catch (err) {
49
- // Auth failures are actionable, not transient — surface them instead of
50
- // reporting "temporarily unavailable, retry".
51
- if (err instanceof HaystackApiError && err.isAuthError)
52
- throw err;
53
- return mergePublicState(meta, undefined, false);
54
- }
55
- }
56
- export async function fetchPrStatePayload(ref) {
57
- const pr = parsePrRef(ref, 'pr get');
58
- const token = await loadToken({ owner: pr.owner, repo: pr.repo });
59
- if (!token)
60
- throw new Error('Not authenticated. Run `haystack login` first.');
61
- const meta = await fetchGitHubJson(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, token);
62
- const [synthesis, manifest, merge] = await Promise.all([
63
- fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
64
- fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
65
- fetchMergePublicState(meta, pr.prNumber, pr.owner, pr.repo, token),
66
- ]);
67
- const payload = {
68
- ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
69
- title: meta.title,
70
- head_sha: meta.head.sha,
71
- triage: synthesis
72
- ? {
73
- status: 'complete',
74
- verdict: toPublicVerdict(synthesis.analysisVerdict, synthesis.haystackRating),
75
- findings_count: synthesis.synthesisDisplay?.length ?? 0,
76
- }
77
- : { status: 'pending', verdict: null, findings_count: 0 },
78
- merge,
79
- };
80
- if (manifest) {
81
- const fixed = manifest.fixedItems.length;
82
- const remaining = manifest.leftForReview.length;
83
- payload.fixer = {
84
- status: 'completed',
85
- result_sha: manifest.resultSha ?? meta.head.sha,
86
- summary: `Fixed ${fixed} finding${fixed === 1 ? '' : 's'}; ${remaining} remain${remaining === 1 ? 's' : ''} for review.`,
87
- };
88
- }
89
- return withSchema('pr', payload);
90
- }
91
- export async function prReadCommand(ref, options) {
92
- const payload = await fetchPrStatePayload(ref);
93
- if (options.json) {
94
- console.log(JSON.stringify(payload, null, 2));
95
- return;
96
- }
97
- const triage = payload.triage;
98
- const merge = payload.merge;
99
- console.log(`\n ${chalk.bold(String(payload.ref))} ${chalk.dim(String(payload.title))}`);
100
- console.log(` ${chalk.dim('triage:')} ${triage.status}${triage.verdict ? ` · ${triage.verdict}` : ''} · ${triage.findings_count} findings`);
101
- console.log(` ${chalk.dim('merge:')} ${merge.status} · ${merge.reason}`);
102
- if (merge.action)
103
- console.log(` ${chalk.dim('next:')} ${merge.action}`);
104
- console.log('');
105
- }