@haystackeditor/cli 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -455
- package/dist/capture/app-config.js +25 -1
- package/dist/commands/capture-brief.js +41 -32
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +53 -7
- package/dist/commands/init.js +7 -2
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/verify.js +68 -13
- package/dist/index.js +27 -923
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +0 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
package/dist/commands/hooks.js
DELETED
|
@@ -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
|
-
}
|
package/dist/commands/inbox.js
DELETED
|
@@ -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
|
-
}
|
package/dist/commands/mcp.js
DELETED
|
@@ -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
|
-
}
|