@haystackeditor/cli 0.8.1 → 0.10.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 +93 -87
- package/dist/assets/hooks/llm-rules-template.md +21 -0
- package/dist/assets/hooks/package.json +2 -2
- package/dist/assets/hooks/scripts/pre-push.sh +20 -0
- package/dist/assets/skills/prepare-haystack.md +323 -0
- package/dist/assets/skills/secrets.md +164 -0
- package/dist/assets/skills/setup-external-sandbox.md +243 -0
- package/dist/assets/skills/setup-haystack.md +639 -0
- package/dist/assets/skills/submit.md +154 -0
- package/dist/assets/templates/CLAUDE.md.snippet +42 -0
- package/dist/assets/templates/haystack.yml +193 -0
- package/dist/commands/check-pending.d.ts +19 -0
- package/dist/commands/check-pending.js +217 -0
- package/dist/commands/config.d.ts +13 -21
- package/dist/commands/config.js +278 -92
- package/dist/commands/dismiss.d.ts +17 -0
- package/dist/commands/dismiss.js +201 -0
- package/dist/commands/init.js +25 -28
- package/dist/commands/install-session-hooks.d.ts +16 -0
- package/dist/commands/install-session-hooks.js +302 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +365 -0
- package/dist/commands/pr-status.d.ts +16 -0
- package/dist/commands/pr-status.js +188 -0
- package/dist/commands/setup.d.ts +13 -0
- package/dist/commands/setup.js +496 -0
- package/dist/commands/skills.d.ts +2 -2
- package/dist/commands/skills.js +51 -186
- package/dist/commands/submit.d.ts +23 -0
- package/dist/commands/submit.js +456 -0
- package/dist/commands/triage.d.ts +16 -0
- package/dist/commands/triage.js +354 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +344 -4
- package/dist/tools/detect.d.ts +50 -0
- package/dist/tools/detect.js +853 -0
- package/dist/tools/fixtures.d.ts +38 -0
- package/dist/tools/fixtures.js +199 -0
- package/dist/tools/setup.d.ts +43 -0
- package/dist/tools/setup.js +597 -0
- package/dist/triage/prompts.d.ts +31 -0
- package/dist/triage/prompts.js +296 -0
- package/dist/triage/runner.d.ts +21 -0
- package/dist/triage/runner.js +339 -0
- package/dist/triage/traces.d.ts +20 -0
- package/dist/triage/traces.js +305 -0
- package/dist/triage/types.d.ts +47 -0
- package/dist/triage/types.js +7 -0
- package/dist/types.d.ts +1387 -191
- package/dist/types.js +254 -2
- package/dist/utils/analysis-api.d.ts +108 -0
- package/dist/utils/analysis-api.js +194 -0
- package/dist/utils/config.js +1 -1
- package/dist/utils/git.d.ts +80 -0
- package/dist/utils/git.js +302 -0
- package/dist/utils/github-api.d.ts +83 -0
- package/dist/utils/github-api.js +266 -0
- package/dist/utils/pending-state.d.ts +40 -0
- package/dist/utils/pending-state.js +86 -0
- package/dist/utils/secrets.js +3 -3
- package/dist/utils/skill.js +257 -0
- package/package.json +11 -9
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Submit PR via Haystack
|
|
2
|
+
|
|
3
|
+
**Your job**: Submit your changes as a PR using `haystack submit` instead of `gh pr create` or GitHub API.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why Use Haystack Submit?
|
|
8
|
+
|
|
9
|
+
When you create PRs through Haystack, they enter the **auto-merge queue**:
|
|
10
|
+
|
|
11
|
+
1. **Haystack analyzes** your changes automatically
|
|
12
|
+
2. **If approved**: PR is marked ready-to-merge and merged automatically
|
|
13
|
+
3. **If issues found**: PR is routed for review with detailed feedback
|
|
14
|
+
|
|
15
|
+
This is faster than waiting for human review on every change.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Command Reference
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Default: Submit to auto-merge queue
|
|
23
|
+
haystack submit
|
|
24
|
+
|
|
25
|
+
# Request Haystack review (for complex/risky changes)
|
|
26
|
+
haystack submit --review
|
|
27
|
+
|
|
28
|
+
# With custom title
|
|
29
|
+
haystack submit --title "Fix authentication bug"
|
|
30
|
+
|
|
31
|
+
# Target different base branch
|
|
32
|
+
haystack submit --base develop
|
|
33
|
+
|
|
34
|
+
# With markdown PR body (write to file first to preserve formatting)
|
|
35
|
+
cat > /tmp/pr-body.md << 'EOF'
|
|
36
|
+
## Summary
|
|
37
|
+
- Fixed authentication bug in session handling
|
|
38
|
+
- Added integration tests for token refresh
|
|
39
|
+
|
|
40
|
+
## Test plan
|
|
41
|
+
- [x] Auth flow works end-to-end
|
|
42
|
+
EOF
|
|
43
|
+
haystack submit --body-file /tmp/pr-body.md
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> **Tip**: Always use `--body-file` instead of `--body` when the description contains markdown.
|
|
47
|
+
> The `--body` flag is a single CLI argument that mangles newlines and special characters (`#`, `*`, etc.).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## When to Use Each Mode
|
|
52
|
+
|
|
53
|
+
### Auto-Merge (Default)
|
|
54
|
+
|
|
55
|
+
Use for straightforward changes that don't need human review:
|
|
56
|
+
|
|
57
|
+
- Bug fixes with clear scope
|
|
58
|
+
- Small features with tests
|
|
59
|
+
- Refactoring with no behavior change
|
|
60
|
+
- Documentation updates
|
|
61
|
+
- Dependency updates
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
haystack submit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Review Mode
|
|
68
|
+
|
|
69
|
+
> **Do NOT use `--review` unless the user explicitly asks for human review.**
|
|
70
|
+
> The default auto-merge queue already runs full Haystack analysis. Using `--review`
|
|
71
|
+
> blocks auto-merge until a human approves, which delays merging unnecessarily.
|
|
72
|
+
|
|
73
|
+
Use `--review` only when the user specifically requests human review:
|
|
74
|
+
|
|
75
|
+
- User says "get someone to review this" or "needs human eyes"
|
|
76
|
+
- User explicitly passes `--review` in their instructions
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
haystack submit --review
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Workflow
|
|
85
|
+
|
|
86
|
+
1. **Make your changes** on a feature branch
|
|
87
|
+
2. **Commit** your changes: `git add . && git commit -m "..."`
|
|
88
|
+
3. **Submit** via Haystack: `haystack submit`
|
|
89
|
+
|
|
90
|
+
Haystack will:
|
|
91
|
+
- Create a `haystack/<your-branch>` branch
|
|
92
|
+
- Push to origin
|
|
93
|
+
- Create the PR with appropriate labels
|
|
94
|
+
- Trigger analysis automatically
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Prerequisites
|
|
99
|
+
|
|
100
|
+
Before using `haystack submit`, ensure you're logged in:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
haystack login
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This uses GitHub device flow - no tokens to copy/paste.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Important Notes
|
|
111
|
+
|
|
112
|
+
- **Always use `haystack submit`** instead of `gh pr create` when working in Haystack-enabled repos
|
|
113
|
+
- The command creates a `haystack/` prefixed branch automatically
|
|
114
|
+
- Labels are applied automatically based on your chosen mode
|
|
115
|
+
- You don't need to push manually - the command handles it
|
|
116
|
+
- Use `haystack triage <pr>` to view full analysis results (rating, findings, agent fix prompts) for any PR at any time
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Example Session
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Agent: I've fixed the authentication bug. Let me submit this as a PR.
|
|
124
|
+
|
|
125
|
+
$ haystack submit --title "Fix session expiry handling"
|
|
126
|
+
|
|
127
|
+
Haystack Submit
|
|
128
|
+
|
|
129
|
+
✓ Authenticated
|
|
130
|
+
✓ Repository: acme/webapp
|
|
131
|
+
Current branch: fix-auth
|
|
132
|
+
Base branch: main
|
|
133
|
+
Submission type: Auto-merge queue
|
|
134
|
+
|
|
135
|
+
Creating branch haystack/fix-auth...
|
|
136
|
+
✓ Created branch: haystack/fix-auth
|
|
137
|
+
✓ Branch pushed
|
|
138
|
+
✓ Pull request created: #142
|
|
139
|
+
✓ Applied label: haystack:auto-merge
|
|
140
|
+
|
|
141
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
142
|
+
|
|
143
|
+
Pull Request Created!
|
|
144
|
+
|
|
145
|
+
URL: https://github.com/acme/webapp/pull/142
|
|
146
|
+
Title: Fix session expiry handling
|
|
147
|
+
Branch: haystack/fix-auth → main
|
|
148
|
+
Type: Auto-merge queue
|
|
149
|
+
Label: haystack:auto-merge
|
|
150
|
+
|
|
151
|
+
Haystack will analyze this PR and:
|
|
152
|
+
• Auto-merge if approved
|
|
153
|
+
• Route for review if issues found
|
|
154
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Haystack Verification
|
|
2
|
+
|
|
3
|
+
This project uses Haystack for PR verification. Configuration is in `.haystack.yml`.
|
|
4
|
+
|
|
5
|
+
## For the AI Agent
|
|
6
|
+
|
|
7
|
+
When making changes to this codebase:
|
|
8
|
+
|
|
9
|
+
1. **After completing changes**, run verification:
|
|
10
|
+
- Use the `haystack_get_verify_targets` tool to see what URLs to check
|
|
11
|
+
- Use `haystack_get_auth_config` to get auth bypass instructions
|
|
12
|
+
- Start the dev server with auth bypass env vars
|
|
13
|
+
- Navigate to each verification URL and confirm it loads
|
|
14
|
+
|
|
15
|
+
2. **If `.haystack.yml` doesn't exist**, help the user create one:
|
|
16
|
+
- Use `haystack_setup_start` to begin the wizard
|
|
17
|
+
- Ask each question and call `haystack_setup_answer` with responses
|
|
18
|
+
|
|
19
|
+
3. **Fixture data** is available via `haystack_load_fixtures`:
|
|
20
|
+
- This loads test data from staging/local files
|
|
21
|
+
- Use this instead of real API calls when verifying
|
|
22
|
+
|
|
23
|
+
## Quick Reference
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
# .haystack.yml structure
|
|
27
|
+
dev:
|
|
28
|
+
command: pnpm dev # How to start dev server
|
|
29
|
+
port: 3000 # What port it runs on
|
|
30
|
+
|
|
31
|
+
auth:
|
|
32
|
+
strategy: bypass # How to skip auth
|
|
33
|
+
env: "SKIP_AUTH=true" # Env var to set
|
|
34
|
+
|
|
35
|
+
fixtures: # Test data sources
|
|
36
|
+
- pattern: "/api/*"
|
|
37
|
+
source: "https://staging.example.com/api"
|
|
38
|
+
|
|
39
|
+
verify: # Pages to check
|
|
40
|
+
- url: "/"
|
|
41
|
+
wait_for: "#app"
|
|
42
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Haystack Verification Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
#
|
|
5
|
+
# This file tells Haystack how to verify that PRs actually work.
|
|
6
|
+
# When a PR is opened, Haystack spins up a sandbox with your code and
|
|
7
|
+
# runs these verification flows to ensure changes work correctly.
|
|
8
|
+
#
|
|
9
|
+
# For setup help, run: /verify setup
|
|
10
|
+
# =============================================================================
|
|
11
|
+
|
|
12
|
+
name: my-saas-app
|
|
13
|
+
|
|
14
|
+
# -----------------------------------------------------------------------------
|
|
15
|
+
# Dev Server - How to run your app
|
|
16
|
+
# -----------------------------------------------------------------------------
|
|
17
|
+
dev:
|
|
18
|
+
command: pnpm dev
|
|
19
|
+
port: 3000
|
|
20
|
+
ready_pattern: "ready in" # Pattern in stdout that means server is ready
|
|
21
|
+
cwd: "." # Working directory (relative to repo root)
|
|
22
|
+
env:
|
|
23
|
+
NODE_ENV: development
|
|
24
|
+
|
|
25
|
+
# -----------------------------------------------------------------------------
|
|
26
|
+
# Authentication - How to bypass/mock auth in the sandbox
|
|
27
|
+
# -----------------------------------------------------------------------------
|
|
28
|
+
auth:
|
|
29
|
+
strategy: bypass
|
|
30
|
+
env: "SKIP_AUTH=true"
|
|
31
|
+
|
|
32
|
+
# Other options:
|
|
33
|
+
# strategy: mock_token
|
|
34
|
+
# env: "AUTH_TOKEN" # Set actual value as Haystack secret
|
|
35
|
+
|
|
36
|
+
# strategy: test_account
|
|
37
|
+
# username_env: "TEST_USER"
|
|
38
|
+
# password_env: "TEST_PASS"
|
|
39
|
+
# login_url: "/login"
|
|
40
|
+
|
|
41
|
+
# -----------------------------------------------------------------------------
|
|
42
|
+
# Fixtures - Where to get test data (so sandbox doesn't need real data)
|
|
43
|
+
# -----------------------------------------------------------------------------
|
|
44
|
+
fixtures:
|
|
45
|
+
# Pull user data from staging API
|
|
46
|
+
- pattern: "/api/users/*"
|
|
47
|
+
source: "https://staging.myapp.com/api/users"
|
|
48
|
+
headers:
|
|
49
|
+
Authorization: "Bearer $STAGING_API_TOKEN"
|
|
50
|
+
|
|
51
|
+
# Use local fixture file for config
|
|
52
|
+
- pattern: "/api/config"
|
|
53
|
+
source: "file://fixtures/config.json"
|
|
54
|
+
|
|
55
|
+
# Generate dashboard data dynamically
|
|
56
|
+
- pattern: "/api/dashboard"
|
|
57
|
+
source: "script://fixtures/generate-dashboard.ts"
|
|
58
|
+
|
|
59
|
+
# Let public endpoints go through
|
|
60
|
+
- pattern: "/api/public/*"
|
|
61
|
+
source: passthrough
|
|
62
|
+
|
|
63
|
+
# -----------------------------------------------------------------------------
|
|
64
|
+
# Verification Flows - What to actually test
|
|
65
|
+
# -----------------------------------------------------------------------------
|
|
66
|
+
# These are the flows the agent executes in the sandbox to verify PRs work.
|
|
67
|
+
# Think of them like E2E tests, but run by an AI that can adapt and report.
|
|
68
|
+
|
|
69
|
+
flows:
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Basic smoke test - runs on every PR
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
- name: "Homepage loads"
|
|
74
|
+
description: "Verify the homepage renders without errors"
|
|
75
|
+
trigger: always
|
|
76
|
+
steps:
|
|
77
|
+
- action: navigate
|
|
78
|
+
url: "/"
|
|
79
|
+
- action: wait_for
|
|
80
|
+
selector: "#app"
|
|
81
|
+
timeout_ms: 10000
|
|
82
|
+
- action: assert_no_errors
|
|
83
|
+
error_selectors:
|
|
84
|
+
- ".error-toast"
|
|
85
|
+
- ".error-modal"
|
|
86
|
+
- "[data-testid='error']"
|
|
87
|
+
- action: screenshot
|
|
88
|
+
name: "homepage"
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Dashboard flow - runs when dashboard files change
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
- name: "Dashboard functionality"
|
|
94
|
+
description: "Verify dashboard loads and displays data correctly"
|
|
95
|
+
trigger: on_change
|
|
96
|
+
watch_patterns:
|
|
97
|
+
- "src/components/dashboard/**"
|
|
98
|
+
- "src/pages/dashboard.tsx"
|
|
99
|
+
steps:
|
|
100
|
+
- action: navigate
|
|
101
|
+
url: "/dashboard"
|
|
102
|
+
- action: wait_for
|
|
103
|
+
selector: ".dashboard-grid"
|
|
104
|
+
- action: assert_exists
|
|
105
|
+
selector: ".metric-card"
|
|
106
|
+
message: "Dashboard should display metric cards"
|
|
107
|
+
- action: assert_text
|
|
108
|
+
selector: ".welcome-message"
|
|
109
|
+
contains: "Welcome"
|
|
110
|
+
- action: screenshot
|
|
111
|
+
name: "dashboard-loaded"
|
|
112
|
+
# Test interactivity
|
|
113
|
+
- action: click
|
|
114
|
+
selector: "[data-testid='refresh-button']"
|
|
115
|
+
description: "Click refresh button"
|
|
116
|
+
- action: wait_network_idle
|
|
117
|
+
timeout_ms: 5000
|
|
118
|
+
- action: assert_no_errors
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Settings flow - test form interactions
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
- name: "Settings page"
|
|
124
|
+
description: "Verify settings can be viewed and saved"
|
|
125
|
+
trigger: on_change
|
|
126
|
+
watch_patterns:
|
|
127
|
+
- "src/components/settings/**"
|
|
128
|
+
- "src/pages/settings.tsx"
|
|
129
|
+
steps:
|
|
130
|
+
- action: navigate
|
|
131
|
+
url: "/settings"
|
|
132
|
+
- action: wait_for
|
|
133
|
+
selector: "form.settings-form"
|
|
134
|
+
- action: screenshot
|
|
135
|
+
name: "settings-initial"
|
|
136
|
+
# Toggle dark mode
|
|
137
|
+
- action: click
|
|
138
|
+
selector: "#dark-mode-toggle"
|
|
139
|
+
- action: assert_exists
|
|
140
|
+
selector: "body.dark-mode"
|
|
141
|
+
message: "Dark mode should be applied"
|
|
142
|
+
- action: screenshot
|
|
143
|
+
name: "settings-dark-mode"
|
|
144
|
+
# Test form input
|
|
145
|
+
- action: type
|
|
146
|
+
selector: "#display-name"
|
|
147
|
+
text: "Test User"
|
|
148
|
+
- action: click
|
|
149
|
+
selector: "button[type='submit']"
|
|
150
|
+
- action: wait_network_idle
|
|
151
|
+
- action: assert_no_errors
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Checkout flow - critical path, always run
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
- name: "Checkout flow"
|
|
157
|
+
description: "Verify the complete checkout process works"
|
|
158
|
+
trigger: always
|
|
159
|
+
steps:
|
|
160
|
+
- action: navigate
|
|
161
|
+
url: "/products"
|
|
162
|
+
- action: wait_for
|
|
163
|
+
selector: ".product-grid"
|
|
164
|
+
- action: click
|
|
165
|
+
selector: ".product-card:first-child .add-to-cart"
|
|
166
|
+
description: "Add first product to cart"
|
|
167
|
+
- action: wait_for
|
|
168
|
+
selector: ".cart-count"
|
|
169
|
+
- action: assert_text
|
|
170
|
+
selector: ".cart-count"
|
|
171
|
+
contains: "1"
|
|
172
|
+
- action: navigate
|
|
173
|
+
url: "/cart"
|
|
174
|
+
- action: wait_for
|
|
175
|
+
selector: ".cart-items"
|
|
176
|
+
- action: click
|
|
177
|
+
selector: ".checkout-button"
|
|
178
|
+
- action: wait_for
|
|
179
|
+
selector: ".checkout-form"
|
|
180
|
+
- action: screenshot
|
|
181
|
+
name: "checkout-form"
|
|
182
|
+
compare_baseline: true # Visual regression check
|
|
183
|
+
|
|
184
|
+
# -----------------------------------------------------------------------------
|
|
185
|
+
# Global Settings
|
|
186
|
+
# -----------------------------------------------------------------------------
|
|
187
|
+
settings:
|
|
188
|
+
default_timeout_ms: 10000
|
|
189
|
+
error_selectors:
|
|
190
|
+
- ".error-toast"
|
|
191
|
+
- ".error-boundary"
|
|
192
|
+
- "[role='alert'][aria-live='assertive']"
|
|
193
|
+
screenshot_each_step: false # Set true for debugging
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-pending command — Check status of pending Haystack analysis.
|
|
3
|
+
*
|
|
4
|
+
* Reads `.haystack/pending-submit.json`, polls the analysis API if needed,
|
|
5
|
+
* and outputs a two-state verdict:
|
|
6
|
+
* - "Good to merge" (no issues)
|
|
7
|
+
* - "Needs your input" (issues found)
|
|
8
|
+
*
|
|
9
|
+
* Output modes:
|
|
10
|
+
* --hook Minimal single-line for session-start hooks
|
|
11
|
+
* --json Machine-readable JSON
|
|
12
|
+
* (default) Rich terminal output
|
|
13
|
+
*/
|
|
14
|
+
export interface CheckPendingOptions {
|
|
15
|
+
json?: boolean;
|
|
16
|
+
hook?: boolean;
|
|
17
|
+
clear?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function checkPendingCommand(options: CheckPendingOptions): Promise<void>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-pending command — Check status of pending Haystack analysis.
|
|
3
|
+
*
|
|
4
|
+
* Reads `.haystack/pending-submit.json`, polls the analysis API if needed,
|
|
5
|
+
* and outputs a two-state verdict:
|
|
6
|
+
* - "Good to merge" (no issues)
|
|
7
|
+
* - "Needs your input" (issues found)
|
|
8
|
+
*
|
|
9
|
+
* Output modes:
|
|
10
|
+
* --hook Minimal single-line for session-start hooks
|
|
11
|
+
* --json Machine-readable JSON
|
|
12
|
+
* (default) Rich terminal output
|
|
13
|
+
*/
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
|
|
16
|
+
import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
|
|
17
|
+
import { findExistingPR } from '../utils/github-api.js';
|
|
18
|
+
import { loadToken } from './login.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Polling
|
|
21
|
+
// ============================================================================
|
|
22
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
23
|
+
const POLL_TIMEOUT_MS = 30_000;
|
|
24
|
+
/**
|
|
25
|
+
* Poll the result endpoint until analysis is ready or timeout.
|
|
26
|
+
*/
|
|
27
|
+
async function waitForAnalysis(owner, repo, prNumber, timeoutMs, token) {
|
|
28
|
+
const deadline = Date.now() + timeoutMs;
|
|
29
|
+
let consecutiveErrors = 0;
|
|
30
|
+
while (Date.now() < deadline) {
|
|
31
|
+
const result = await checkAnalysisReady(owner, repo, prNumber, token);
|
|
32
|
+
if (result.status === 'ready')
|
|
33
|
+
return 'completed';
|
|
34
|
+
if (result.status === 'error') {
|
|
35
|
+
consecutiveErrors++;
|
|
36
|
+
if (consecutiveErrors >= 5)
|
|
37
|
+
return 'failed';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
consecutiveErrors = 0;
|
|
41
|
+
}
|
|
42
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
43
|
+
}
|
|
44
|
+
return 'pending';
|
|
45
|
+
}
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Output formatting
|
|
48
|
+
// ============================================================================
|
|
49
|
+
function printHookOutput(pending) {
|
|
50
|
+
if (!pending.analysisResult) {
|
|
51
|
+
console.log(`[Haystack] PR #${pending.prNumber}: Analysis in progress...`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const result = pending.analysisResult;
|
|
55
|
+
if (result.state === 'good-to-merge') {
|
|
56
|
+
if (result.autoMerged) {
|
|
57
|
+
console.log(`[Haystack] \u2705 PR #${pending.prNumber} "${getBranchLabel(pending)}": Auto-merged`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(`[Haystack] \u2705 PR #${pending.prNumber} "${getBranchLabel(pending)}": Good to merge`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const parts = [];
|
|
65
|
+
if (result.bugCount > 0)
|
|
66
|
+
parts.push(`${result.bugCount} bug(s)`);
|
|
67
|
+
if (result.ruleViolationCount > 0)
|
|
68
|
+
parts.push(`${result.ruleViolationCount} rule violation(s)`);
|
|
69
|
+
if (parts.length === 0)
|
|
70
|
+
parts.push(`${result.warningCount} warning(s)`);
|
|
71
|
+
console.log(`[Haystack] \u26A0\uFE0F PR #${pending.prNumber} "${getBranchLabel(pending)}": Needs your input (${parts.join(', ')})`);
|
|
72
|
+
console.log(` Review: ${result.reviewUrl}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function printRichOutput(pending) {
|
|
76
|
+
console.log(chalk.bold('\n' + '\u2501'.repeat(50)));
|
|
77
|
+
console.log(chalk.bold(` Haystack Analysis: PR #${pending.prNumber}`));
|
|
78
|
+
console.log(chalk.bold('\u2501'.repeat(50) + '\n'));
|
|
79
|
+
console.log(` ${chalk.dim('Branch:')} ${pending.branch} \u2192 ${pending.baseBranch}`);
|
|
80
|
+
console.log(` ${chalk.dim('URL:')} ${chalk.cyan(pending.prUrl)}`);
|
|
81
|
+
if (!pending.analysisResult) {
|
|
82
|
+
if (pending.status === 'polling') {
|
|
83
|
+
console.log(` ${chalk.dim('Status:')} ${chalk.yellow('Analysis in progress...')}`);
|
|
84
|
+
}
|
|
85
|
+
else if (pending.status === 'failed') {
|
|
86
|
+
console.log(` ${chalk.dim('Status:')} ${chalk.red('Analysis failed')}`);
|
|
87
|
+
}
|
|
88
|
+
else if (pending.status === 'stale') {
|
|
89
|
+
console.log(` ${chalk.dim('Status:')} ${chalk.dim('Stale (submitted > 2h ago)')}`);
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const result = pending.analysisResult;
|
|
95
|
+
if (result.state === 'good-to-merge') {
|
|
96
|
+
if (result.autoMerged) {
|
|
97
|
+
console.log(` ${chalk.dim('Verdict:')} ${chalk.green.bold('Auto-merged')}`);
|
|
98
|
+
console.log(` ${chalk.dim('Issues:')} ${chalk.green('None — PR was automatically merged')}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(` ${chalk.dim('Verdict:')} ${chalk.green.bold('Good to merge')}`);
|
|
102
|
+
console.log(` ${chalk.dim('Issues:')} ${chalk.green('None')}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(` ${chalk.dim('Verdict:')} ${chalk.yellow.bold('Needs your input')}`);
|
|
107
|
+
console.log(` ${chalk.dim('Issues:')} ${result.summary}`);
|
|
108
|
+
console.log('');
|
|
109
|
+
for (const issue of result.issues) {
|
|
110
|
+
const icon = issue.severity === 'error' ? chalk.red('\u2717') : chalk.yellow('!');
|
|
111
|
+
const loc = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
112
|
+
console.log(` ${icon} ${chalk.dim(loc)} ${issue.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
console.log(`\n ${chalk.dim('Review:')} ${chalk.cyan(result.reviewUrl)}`);
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
function printJsonOutput(pending) {
|
|
119
|
+
const output = {
|
|
120
|
+
prNumber: pending.prNumber,
|
|
121
|
+
prUrl: pending.prUrl,
|
|
122
|
+
branch: pending.branch,
|
|
123
|
+
baseBranch: pending.baseBranch,
|
|
124
|
+
status: pending.status,
|
|
125
|
+
submittedAt: pending.submittedAt,
|
|
126
|
+
verdict: pending.analysisResult?.state || null,
|
|
127
|
+
bugCount: pending.analysisResult?.bugCount || 0,
|
|
128
|
+
warningCount: pending.analysisResult?.warningCount || 0,
|
|
129
|
+
ruleViolationCount: pending.analysisResult?.ruleViolationCount || 0,
|
|
130
|
+
summary: pending.analysisResult?.summary || null,
|
|
131
|
+
issues: pending.analysisResult?.issues || [],
|
|
132
|
+
reviewUrl: pending.analysisResult?.reviewUrl || null,
|
|
133
|
+
autoMerged: pending.analysisResult?.autoMerged || false,
|
|
134
|
+
};
|
|
135
|
+
console.log(JSON.stringify(output, null, 2));
|
|
136
|
+
}
|
|
137
|
+
function getBranchLabel(pending) {
|
|
138
|
+
return pending.branch.replace(/^refs\/heads\//, '');
|
|
139
|
+
}
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Main command
|
|
142
|
+
// ============================================================================
|
|
143
|
+
export async function checkPendingCommand(options) {
|
|
144
|
+
// Handle --clear
|
|
145
|
+
if (options.clear) {
|
|
146
|
+
clearPendingSubmit();
|
|
147
|
+
if (!options.hook) {
|
|
148
|
+
console.log(chalk.dim('Pending submit state cleared.'));
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Load pending state
|
|
153
|
+
const pending = loadPendingSubmit();
|
|
154
|
+
if (!pending) {
|
|
155
|
+
if (options.json) {
|
|
156
|
+
console.log(JSON.stringify({ status: 'none' }));
|
|
157
|
+
}
|
|
158
|
+
else if (!options.hook) {
|
|
159
|
+
// Silent for hook mode — no pending submits is fine
|
|
160
|
+
console.log(chalk.dim('No pending submits.\n'));
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Load token for private repo auth (best-effort, don't block if unavailable)
|
|
165
|
+
const token = await loadToken() || undefined;
|
|
166
|
+
// If still polling, try to resolve it
|
|
167
|
+
if (pending.status === 'polling') {
|
|
168
|
+
// Quick check: is the PR still open?
|
|
169
|
+
try {
|
|
170
|
+
const pr = await findExistingPR(pending.owner, pending.repo, pending.branch);
|
|
171
|
+
if (!pr) {
|
|
172
|
+
// PR was closed or merged
|
|
173
|
+
updatePendingSubmit({ status: 'stale', resolvedAt: new Date().toISOString() });
|
|
174
|
+
pending.status = 'stale';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Auth not available or API error — continue with polling
|
|
179
|
+
}
|
|
180
|
+
if (pending.status === 'polling') {
|
|
181
|
+
// For hook mode, do a single quick poll (no waiting)
|
|
182
|
+
const timeout = options.hook ? 5_000 : POLL_TIMEOUT_MS;
|
|
183
|
+
const result = await waitForAnalysis(pending.owner, pending.repo, pending.prNumber, timeout, token);
|
|
184
|
+
if (result === 'completed') {
|
|
185
|
+
// Fetch the actual analysis results
|
|
186
|
+
try {
|
|
187
|
+
const verdict = await fetchAnalysisResults(pending.owner, pending.repo, pending.prNumber, token);
|
|
188
|
+
updatePendingSubmit({
|
|
189
|
+
status: 'completed',
|
|
190
|
+
analysisResult: verdict,
|
|
191
|
+
resolvedAt: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
pending.status = 'completed';
|
|
194
|
+
pending.analysisResult = verdict;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Could not fetch results — leave as polling
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (result === 'failed') {
|
|
201
|
+
updatePendingSubmit({ status: 'failed', resolvedAt: new Date().toISOString() });
|
|
202
|
+
pending.status = 'failed';
|
|
203
|
+
}
|
|
204
|
+
// 'pending' — leave as-is for next check
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Output based on mode
|
|
208
|
+
if (options.json) {
|
|
209
|
+
printJsonOutput(pending);
|
|
210
|
+
}
|
|
211
|
+
else if (options.hook) {
|
|
212
|
+
printHookOutput(pending);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
printRichOutput(pending);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config commands - manage
|
|
2
|
+
* Config commands - manage preferences
|
|
3
|
+
*
|
|
4
|
+
* Repo-level preferences (sandbox_enabled, auto_merge) are stored in .haystack.json.
|
|
5
|
+
* User-level preferences (agentic_tool) are stored on the Haystack Platform API.
|
|
3
6
|
*/
|
|
4
7
|
type AgenticTool = 'opencode' | 'claude-code' | 'codex';
|
|
5
|
-
/**
|
|
6
|
-
* Get current sandbox status
|
|
7
|
-
*/
|
|
8
8
|
export declare function getSandboxStatus(): Promise<void>;
|
|
9
|
-
/**
|
|
10
|
-
* Enable sandbox mode
|
|
11
|
-
*/
|
|
12
9
|
export declare function enableSandbox(): Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* Disable sandbox mode
|
|
15
|
-
*/
|
|
16
10
|
export declare function disableSandbox(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Handle sandbox subcommand
|
|
19
|
-
*/
|
|
20
11
|
export declare function handleSandbox(action?: string): Promise<void>;
|
|
21
|
-
/**
|
|
22
|
-
* Get current agentic tool setting
|
|
23
|
-
*/
|
|
24
12
|
export declare function getAgenticToolStatus(): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
* Set agentic tool preference
|
|
27
|
-
*/
|
|
28
13
|
export declare function setAgenticTool(tool: AgenticTool): Promise<void>;
|
|
14
|
+
export declare function handleAgenticTool(tool?: string): Promise<void>;
|
|
29
15
|
/**
|
|
30
|
-
*
|
|
16
|
+
* Check if auto-merge is enabled for the current repo.
|
|
17
|
+
* Returns false if .haystack.json is missing or on error.
|
|
31
18
|
*/
|
|
32
|
-
export declare function
|
|
19
|
+
export declare function isAutoMergeEnabled(): Promise<boolean>;
|
|
20
|
+
export declare function getAutoMergeStatus(): Promise<void>;
|
|
21
|
+
export declare function enableAutoMerge(): Promise<void>;
|
|
22
|
+
export declare function disableAutoMerge(): Promise<void>;
|
|
23
|
+
export declare function handleAutoMerge(action?: string): Promise<void>;
|
|
24
|
+
export declare function handleWaitForReviewers(action?: string, reviewers?: string[]): Promise<void>;
|
|
33
25
|
export {};
|