@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,226 +0,0 @@
1
- /**
2
- * haystack hooks - Install and manage git hooks for AI agent quality checks
3
- *
4
- * Installs:
5
- * - Pre-commit hook (agent detection, truncation checking, LLM rules review)
6
- * - Git hooks for Haystack quality checks and workflow guidance
7
- */
8
- import chalk from 'chalk';
9
- import { existsSync, accessSync, constants, lstatSync, readFileSync, readdirSync } from 'fs';
10
- import * as path from 'path';
11
- import { execFileSync, execSync } from 'child_process';
12
- import { findGitRoot, copyHookFiles, installHookDeps, copyLlmRulesTemplate, listHookInstallFiles, } from '../utils/hooks.js';
13
- function gitOutput(gitRoot, args) {
14
- return execFileSync('git', args, { cwd: gitRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
15
- }
16
- function configuredHooksPath(gitRoot) {
17
- try {
18
- return gitOutput(gitRoot, ['config', '--get', 'core.hooksPath']);
19
- }
20
- catch (error) {
21
- // `git config --get` exits 1 when the key is unset.
22
- if (error.status === 1)
23
- return undefined;
24
- throw error;
25
- }
26
- }
27
- /**
28
- * Everything `haystack hooks install` would change that Haystack did not
29
- * itself put there: hook files in hooks/ whose content differs from what this
30
- * CLI installs, and a core.hooksPath change (including one that silences
31
- * hooks already active in .git/hooks). Empty means installing is safe.
32
- */
33
- export async function describeHookOverwrites(gitRoot) {
34
- const changes = [];
35
- const hooksDir = path.join(gitRoot, 'hooks');
36
- if (existsSync(hooksDir)) {
37
- for (const file of await listHookInstallFiles()) {
38
- const dest = path.join(hooksDir, file.rel);
39
- let stat;
40
- try {
41
- stat = lstatSync(dest);
42
- }
43
- catch (error) {
44
- if (error.code === 'ENOENT')
45
- continue;
46
- throw error;
47
- }
48
- if (!stat.isFile() || !readFileSync(dest).equals(readFileSync(file.src))) {
49
- changes.push(`overwrite hooks/${file.rel}`);
50
- }
51
- }
52
- }
53
- const hooksPath = configuredHooksPath(gitRoot);
54
- if (hooksPath === undefined) {
55
- const gitHooksDir = path.resolve(gitRoot, gitOutput(gitRoot, ['rev-parse', '--git-path', 'hooks']));
56
- const active = existsSync(gitHooksDir)
57
- ? readdirSync(gitHooksDir).filter(name => !name.endsWith('.sample')).sort()
58
- : [];
59
- if (active.length > 0) {
60
- changes.push(`set core.hooksPath to "hooks", so git stops running ${active.map(name => `.git/hooks/${name}`).join(', ')}`);
61
- }
62
- }
63
- else if (hooksPath !== 'hooks') {
64
- changes.push(`change core.hooksPath from "${hooksPath}" to "hooks"`);
65
- }
66
- return changes;
67
- }
68
- export async function hooksInstall(options) {
69
- console.log(chalk.cyan('\nHaystack Hooks Setup\n'));
70
- // Step 1: Validate git repo
71
- const gitRoot = findGitRoot();
72
- if (!gitRoot) {
73
- console.error(chalk.red('Not a git repository. Run this from inside a git repo.\n'));
74
- process.exit(1);
75
- }
76
- const hooksDir = path.join(gitRoot, 'hooks');
77
- // Step 2: Refuse to replace anything Haystack did not install unless forced.
78
- const overwrites = await describeHookOverwrites(gitRoot);
79
- if (overwrites.length > 0 && !options.force) {
80
- console.error(chalk.yellow('Installing Haystack hooks would:'));
81
- for (const change of overwrites)
82
- console.error(chalk.yellow(` - ${change}`));
83
- console.error(chalk.yellow('Use --force to proceed.\n'));
84
- process.exit(1);
85
- }
86
- if (overwrites.length > 0) {
87
- console.log(chalk.yellow('Overwriting existing hooks...'));
88
- }
89
- // Step 3: Copy hook files
90
- try {
91
- await copyHookFiles(hooksDir);
92
- console.log(chalk.green('✓ Hook scripts installed to hooks/'));
93
- }
94
- catch (err) {
95
- const message = err instanceof Error ? err.message : String(err);
96
- console.error(chalk.red(`Failed to copy hook files: ${message}\n`));
97
- process.exit(1);
98
- }
99
- // Step 4: Install hook dependencies
100
- console.log(chalk.dim('Installing hook dependencies (tree-sitter)...'));
101
- try {
102
- await installHookDeps(hooksDir);
103
- console.log(chalk.green('✓ Hook dependencies installed'));
104
- }
105
- catch {
106
- console.log(chalk.yellow('⚠ Failed to install hook dependencies. Run `npm ci` in hooks/ manually.'));
107
- }
108
- // Step 5: Configure git hooks path
109
- try {
110
- execSync('git config core.hooksPath hooks', { cwd: gitRoot, stdio: 'pipe' });
111
- console.log(chalk.green('✓ Git configured to use hooks/ directory'));
112
- }
113
- catch (err) {
114
- const message = err instanceof Error ? err.message : String(err);
115
- console.error(chalk.red(`Failed to set git hooks path: ${message}\n`));
116
- process.exit(1);
117
- }
118
- // Step 6: Create LLM_RULES.md if needed
119
- try {
120
- const created = await copyLlmRulesTemplate(gitRoot);
121
- if (created) {
122
- console.log(chalk.green('✓ Created LLM_RULES.md (customize this for your project)'));
123
- }
124
- else {
125
- console.log(chalk.dim(' LLM_RULES.md already exists'));
126
- }
127
- }
128
- catch {
129
- console.log(chalk.yellow('⚠ Failed to create LLM_RULES.md'));
130
- }
131
- // Summary
132
- console.log(chalk.green('\n✓ Haystack hooks installed!\n'));
133
- console.log(chalk.dim('Hooks installed:'));
134
- console.log(chalk.dim(' pre-commit Agent detection, truncation check, LLM rules review'));
135
- console.log(chalk.dim(' commit-msg Haystack commit checks'));
136
- console.log(chalk.dim(' post-commit Haystack post-commit checks'));
137
- console.log(chalk.dim(' pre-push Haystack workflow guidance'));
138
- console.log(chalk.dim(' prepare-commit-msg Haystack commit preparation'));
139
- console.log('');
140
- console.log(chalk.dim('Files to commit:'));
141
- console.log(chalk.dim(' hooks/ Hook scripts and TypeScript modules'));
142
- console.log(chalk.dim(' LLM_RULES.md Customize this for your project'));
143
- console.log('');
144
- }
145
- // ============================================================================
146
- // haystack hooks status
147
- // ============================================================================
148
- export async function hooksStatus() {
149
- const gitRoot = findGitRoot();
150
- if (!gitRoot) {
151
- console.error(chalk.red('Not a git repository.\n'));
152
- process.exit(1);
153
- }
154
- console.log(chalk.cyan('\nHaystack Hooks Status\n'));
155
- const hooksDir = path.join(gitRoot, 'hooks');
156
- // Check hooks directory
157
- if (!existsSync(hooksDir)) {
158
- console.log(chalk.red('✗ hooks/ directory not found'));
159
- console.log(chalk.dim(' Run: haystack hooks install\n'));
160
- return;
161
- }
162
- console.log(chalk.green('✓ hooks/ directory exists'));
163
- // Check each hook script
164
- const requiredHooks = [
165
- 'pre-commit',
166
- 'commit-msg',
167
- 'post-commit',
168
- 'pre-push',
169
- 'prepare-commit-msg',
170
- ];
171
- for (const hook of requiredHooks) {
172
- const hookPath = path.join(hooksDir, hook);
173
- if (existsSync(hookPath)) {
174
- try {
175
- accessSync(hookPath, constants.X_OK);
176
- console.log(chalk.green(`✓ ${hook}`));
177
- }
178
- catch {
179
- console.log(chalk.yellow(`⚠ ${hook} (not executable)`));
180
- }
181
- }
182
- else {
183
- console.log(chalk.red(`✗ ${hook} missing`));
184
- }
185
- }
186
- // Check TypeScript modules
187
- const agentContextDir = path.join(hooksDir, 'agent-context');
188
- const truncationDir = path.join(hooksDir, 'truncation-checker');
189
- console.log(existsSync(agentContextDir)
190
- ? chalk.green('✓ agent-context/ module')
191
- : chalk.red('✗ agent-context/ missing'));
192
- console.log(existsSync(truncationDir)
193
- ? chalk.green('✓ truncation-checker/ module')
194
- : chalk.red('✗ truncation-checker/ missing'));
195
- // Check hook dependencies
196
- const nodeModules = path.join(hooksDir, 'node_modules');
197
- console.log(existsSync(nodeModules)
198
- ? chalk.green('✓ Hook dependencies installed')
199
- : chalk.yellow('⚠ Hook dependencies not installed (run npm ci in hooks/)'));
200
- // Check git core.hooksPath
201
- try {
202
- const hooksPath = execSync('git config core.hooksPath', {
203
- cwd: gitRoot,
204
- encoding: 'utf-8',
205
- stdio: ['pipe', 'pipe', 'pipe'],
206
- }).trim();
207
- if (hooksPath === 'hooks') {
208
- console.log(chalk.green('✓ Git core.hooksPath = hooks'));
209
- }
210
- else {
211
- console.log(chalk.yellow(`⚠ Git core.hooksPath = ${hooksPath} (expected: hooks)`));
212
- }
213
- }
214
- catch {
215
- console.log(chalk.red('✗ Git core.hooksPath not set'));
216
- }
217
- // Check LLM_RULES.md
218
- const rulesPath = path.join(gitRoot, 'LLM_RULES.md');
219
- if (existsSync(rulesPath)) {
220
- console.log(chalk.green('✓ LLM_RULES.md'));
221
- }
222
- else {
223
- console.log(chalk.yellow('⚠ LLM_RULES.md not found'));
224
- }
225
- console.log('');
226
- }
@@ -1,137 +0,0 @@
1
- import chalk from 'chalk';
2
- import { loadToken } from '../utils/auth.js';
3
- import { fetchCliIdentity, fetchGitHubJson, fetchHaystackInstallations, haystackJson, } from '../utils/haystack-api.js';
4
- import { withSchema } from '../schema.js';
5
- async function fetchOpenPulls(owner, repo, token) {
6
- const pulls = [];
7
- for (let page = 1;; page++) {
8
- const batch = await fetchGitHubJson(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls?state=open&per_page=100&page=${page}`, token);
9
- pulls.push(...batch);
10
- if (batch.length < 100)
11
- return pulls;
12
- }
13
- }
14
- export function isRelevantToUser(pr, userId) {
15
- return pr.user.id === userId
16
- || (pr.assignees ?? []).some((user) => user.id === userId)
17
- || (pr.requested_reviewers ?? []).some((user) => user.id === userId);
18
- }
19
- async function mapConcurrent(values, concurrency, fn) {
20
- const results = new Array(values.length);
21
- let next = 0;
22
- await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => {
23
- while (next < values.length) {
24
- const index = next++;
25
- results[index] = await fn(values[index]);
26
- }
27
- }));
28
- return results;
29
- }
30
- export function classify(candidate, analysis) {
31
- const findingCount = analysis?.findingsCount ?? 0;
32
- const failedChecks = analysis?.merge?.blocking_checks ?? [];
33
- if (analysis?.analysisStatus === 'error') {
34
- return {
35
- bucket: 'needs_shepherding',
36
- reason: 'Haystack analysis failed.',
37
- action: 'Retry the analysis or inspect the failure.',
38
- };
39
- }
40
- if (!analysis || analysis.analysisStatus === 'pending' || analysis.analysisStatus === 'in_queue') {
41
- return {
42
- bucket: 'analyzing',
43
- reason: 'Haystack is still analyzing this PR.',
44
- action: 'Wait for analysis to complete.',
45
- };
46
- }
47
- if (analysis.analysisVerdict === 'has-issues' || (analysis.haystackRating ?? 5) < 4) {
48
- return {
49
- bucket: 'needs_shepherding',
50
- reason: findingCount > 0
51
- ? `Haystack found ${findingCount} author-actionable finding${findingCount === 1 ? '' : 's'}.`
52
- : 'Haystack found author-actionable issues.',
53
- action: 'Read the triage findings and resolve the issues.',
54
- };
55
- }
56
- if (failedChecks.length > 0 || analysis.merge?.status === 'blocked') {
57
- return {
58
- bucket: 'needs_shepherding',
59
- reason: failedChecks.length > 0
60
- ? `${failedChecks.length} required check${failedChecks.length === 1 ? ' is' : 's are'} failing: ${failedChecks.join(', ')}.`
61
- : (analysis.merge?.reason ?? 'The PR is blocked from merging.'),
62
- action: analysis.merge?.action ?? 'Resolve the merge blockers.',
63
- };
64
- }
65
- if (analysis.analysisVerdict === 'needs-review' || analysis.needsHumanReview) {
66
- return {
67
- bucket: 'needs_review',
68
- reason: 'Analysis is clean, but review policy requires a human reviewer.',
69
- action: 'Complete or request the required review.',
70
- };
71
- }
72
- return {
73
- bucket: 'good_to_merge',
74
- reason: 'Haystack found no blocking issues.',
75
- action: 'No user action is required.',
76
- };
77
- }
78
- export async function fetchInboxPayload(token) {
79
- const [identity, installations] = await Promise.all([
80
- fetchCliIdentity(token),
81
- fetchHaystackInstallations(token),
82
- ]);
83
- const repos = [...new Set(installations.flatMap((i) => i.repositories ?? []).map((r) => r.full_name))];
84
- const batches = await mapConcurrent(repos, 8, async (fullName) => {
85
- const [owner, repo] = fullName.split('/');
86
- if (!owner || !repo)
87
- throw new Error(`Invalid repository returned by Haystack: ${fullName}`);
88
- const pulls = await fetchOpenPulls(owner, repo, token);
89
- return pulls
90
- .filter((pr) => isRelevantToUser(pr, identity.id))
91
- .map((pr) => ({ ...pr, owner, repo }));
92
- });
93
- const candidates = batches.flat();
94
- const refs = candidates.map((pr) => `${pr.owner}/${pr.repo}#${pr.number}`);
95
- const analysis = refs.length === 0
96
- ? {}
97
- : await haystackJson('/api/bulk-analysis-summaries', token, {
98
- method: 'POST',
99
- headers: { 'Content-Type': 'application/json' },
100
- body: JSON.stringify({
101
- prIdentifiers: refs,
102
- prBaseBranches: Object.fromEntries(candidates.map((pr) => [`${pr.owner}/${pr.repo}#${pr.number}`, pr.base.ref])),
103
- prHeadShas: Object.fromEntries(candidates.map((pr) => [`${pr.owner}/${pr.repo}#${pr.number}`, pr.head.sha])),
104
- }),
105
- });
106
- return withSchema('inbox', {
107
- pull_requests: candidates.map((pr) => {
108
- const ref = `${pr.owner}/${pr.repo}#${pr.number}`;
109
- return {
110
- ref,
111
- title: pr.title,
112
- head_sha: pr.head.sha,
113
- ...classify(pr, analysis[ref]),
114
- };
115
- }),
116
- });
117
- }
118
- export async function inboxListCommand(options) {
119
- const token = await loadToken();
120
- if (!token)
121
- throw new Error('Not authenticated. Run `haystack login` first.');
122
- const payload = await fetchInboxPayload(token);
123
- if (options.json) {
124
- console.log(JSON.stringify(payload, null, 2));
125
- return;
126
- }
127
- if (payload.pull_requests.length === 0) {
128
- console.log(chalk.dim('\nNo PRs need your attention.\n'));
129
- return;
130
- }
131
- console.log(chalk.bold('\nHaystack inbox\n'));
132
- for (const pr of payload.pull_requests) {
133
- console.log(`${chalk.cyan(pr.ref)} ${chalk.bold(pr.title)}`);
134
- console.log(` ${pr.reason}`);
135
- console.log(` ${chalk.dim('Next:')} ${pr.action}\n`);
136
- }
137
- }
@@ -1,201 +0,0 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
- import { readFileSync } from 'node:fs';
5
- import { fileURLToPath } from 'node:url';
6
- import { dirname, join } from 'node:path';
7
- import { fetchInboxPayload } from './inbox.js';
8
- import { fetchPrStatePayload } from './pr.js';
9
- import { fetchPrStatusPayload } from './pr-status.js';
10
- import { fetchTriagePayload, fetchTriageHeadSha } from './triage.js';
11
- import { fetchAskPayload } from './ask.js';
12
- import { performOverride, performUndismiss } from './dismiss.js';
13
- import { performRequestReview } from './request-review.js';
14
- import { loadToken } from '../utils/auth.js';
15
- import { triggerAnalysis } from '../utils/analysis-api.js';
16
- import { parsePrRef } from '../utils/pr-ref.js';
17
- import { SCHEMA_VERSIONS } from '../schema.js';
18
- import { withSchema } from '../schema.js';
19
- const refInput = {
20
- type: 'string',
21
- description: 'PR reference: bare number, owner/repo#number, or GitHub URL.',
22
- };
23
- const TOOLS = [
24
- {
25
- name: 'inbox_list',
26
- description: 'List PRs in the user\'s Haystack inbox with the reason and next action.',
27
- inputSchema: { type: 'object', properties: {} },
28
- },
29
- {
30
- name: 'pr_get',
31
- description: 'Get useful state for one PR: triage, minimal merge blockers, and conditional fixer outcome.',
32
- inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
33
- },
34
- {
35
- name: 'triage_get',
36
- description: 'Get the complete Haystack verdict and every customer-facing finding for one PR.',
37
- inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
38
- },
39
- {
40
- name: 'ask_haystack',
41
- description: 'Ask Haystack about a PR. Returns the answer and every customer-facing source consulted.',
42
- inputSchema: {
43
- type: 'object',
44
- properties: {
45
- ref: refInput,
46
- question: { type: 'string', description: 'Question about the pull request.' },
47
- session_id: { type: 'string', description: 'Optional prior Ask session to continue.' },
48
- },
49
- required: ['ref', 'question'],
50
- },
51
- },
52
- {
53
- name: 'pr_status',
54
- description: 'Get which Haystack feed bucket a PR is in (analyzing, good_to_merge, issues, needs_assignment, ...) and why.',
55
- inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
56
- },
57
- {
58
- name: 'dismiss',
59
- description: 'Dismiss analysis findings for a PR (moves it toward "Good to Merge"). Tied to the current head commit; a new commit invalidates it.',
60
- inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
61
- },
62
- {
63
- name: 'mark_reviewed',
64
- description: 'Mark human review as not needed for a PR (moves it toward "Good to Merge"). Tied to the current head commit.',
65
- inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
66
- },
67
- {
68
- name: 'undismiss',
69
- description: 'Undo dismiss/mark-reviewed overrides for a PR, returning it to its original feed bucket.',
70
- inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
71
- },
72
- {
73
- name: 'request_review',
74
- description: 'Tag a PR as needing HUMAN review (adds haystack:needs-review), optionally requesting a specific GitHub user.',
75
- inputSchema: {
76
- type: 'object',
77
- properties: {
78
- ref: refInput,
79
- reviewer: { type: 'string', description: 'Optional GitHub username to request review from.' },
80
- },
81
- required: ['ref'],
82
- },
83
- },
84
- {
85
- name: 'trigger_review',
86
- description: 'Trigger a fresh Haystack MACHINE analysis of a PR\'s current head (equivalent to `haystack review`). Returns immediately; poll triage_get for results.',
87
- inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
88
- },
89
- {
90
- name: 'schema',
91
- description: 'Return the committed JSON Schema for a Haystack machine response.',
92
- inputSchema: {
93
- type: 'object',
94
- properties: { name: { type: 'string', enum: Object.keys(SCHEMA_VERSIONS) } },
95
- required: ['name'],
96
- },
97
- },
98
- ];
99
- async function readSchema(name) {
100
- const major = SCHEMA_VERSIONS[name].split('.')[0];
101
- const here = dirname(fileURLToPath(import.meta.url));
102
- return JSON.parse(readFileSync(join(here, '..', '..', 'schemas', `${name}.v${major}.json`), 'utf8'));
103
- }
104
- export async function runMcpServer() {
105
- const server = new Server({ name: 'haystack-cli', version: '1.0.0' }, { capabilities: { tools: {} } });
106
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
107
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
108
- const { name, arguments: args } = request.params;
109
- const input = (args ?? {});
110
- const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
111
- const fail = (message) => ({ isError: true, content: [{ type: 'text', text: message }] });
112
- try {
113
- if (name === 'inbox_list') {
114
- const token = await loadToken();
115
- if (!token)
116
- return fail('Not authenticated. Run `haystack login` first.');
117
- return ok(await fetchInboxPayload(token));
118
- }
119
- if (name === 'pr_get') {
120
- if (typeof input.ref !== 'string')
121
- return fail('ref required');
122
- return ok(await fetchPrStatePayload(input.ref));
123
- }
124
- if (name === 'triage_get') {
125
- if (typeof input.ref !== 'string')
126
- return fail('ref required');
127
- return ok(await fetchTriagePayload(input.ref));
128
- }
129
- if (name === 'ask_haystack') {
130
- if (typeof input.ref !== 'string' || typeof input.question !== 'string')
131
- return fail('ref and question required');
132
- return ok(await fetchAskPayload(input.ref, input.question, {
133
- session: typeof input.session_id === 'string' ? input.session_id : undefined,
134
- }));
135
- }
136
- if (name === 'pr_status') {
137
- if (typeof input.ref !== 'string')
138
- return fail('ref required');
139
- return ok(await fetchPrStatusPayload(input.ref));
140
- }
141
- if (name === 'dismiss' || name === 'mark_reviewed') {
142
- if (typeof input.ref !== 'string')
143
- return fail('ref required');
144
- return ok(withSchema('action', await performOverride(input.ref, name === 'dismiss' ? 'findings-dismissed' : 'review-not-needed', name === 'dismiss' ? 'dismiss' : 'mark-reviewed')));
145
- }
146
- if (name === 'undismiss') {
147
- if (typeof input.ref !== 'string')
148
- return fail('ref required');
149
- return ok(withSchema('action', await performUndismiss(input.ref)));
150
- }
151
- if (name === 'request_review') {
152
- if (typeof input.ref !== 'string')
153
- return fail('ref required');
154
- return ok(withSchema('action', await performRequestReview(input.ref, typeof input.reviewer === 'string' ? input.reviewer : undefined)));
155
- }
156
- if (name === 'trigger_review') {
157
- if (typeof input.ref !== 'string')
158
- return fail('ref required');
159
- const pr = parsePrRef(input.ref, 'review');
160
- const token = await loadToken({ owner: pr.owner, repo: pr.repo });
161
- if (!token)
162
- return fail('Not authenticated. Run `haystack login` first.');
163
- const ref = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
164
- // Resolve the current head so ingestion's dedup compares commits — an
165
- // in-flight run for an OLDER head must be superseded, not reused
166
- // (same degradation as `haystack review`: continue head-less on failure).
167
- let headSha;
168
- try {
169
- headSha = await fetchTriageHeadSha(pr, token) ?? undefined;
170
- }
171
- catch {
172
- headSha = undefined;
173
- }
174
- const trigger = await triggerAnalysis(pr.owner, pr.repo, pr.prNumber, token, headSha);
175
- if (trigger.status !== 'started' && trigger.status !== 'already_processing') {
176
- return fail(trigger.message);
177
- }
178
- return ok(withSchema('action', {
179
- action: 'trigger_review',
180
- status: 'ok',
181
- ref,
182
- detail: 'Analysis triggered; poll triage_get for results.',
183
- request_id: trigger.requestId,
184
- run_status: trigger.status,
185
- next: null,
186
- }));
187
- }
188
- if (name === 'schema') {
189
- const schema = input.name;
190
- if (!(schema in SCHEMA_VERSIONS))
191
- return fail(`unknown schema; choose one of ${Object.keys(SCHEMA_VERSIONS).join(', ')}`);
192
- return ok(await readSchema(schema));
193
- }
194
- return fail(`unknown tool: ${name}`);
195
- }
196
- catch (error) {
197
- return fail(error instanceof Error ? error.message : String(error));
198
- }
199
- });
200
- await server.connect(new StdioServerTransport());
201
- }