@haystackeditor/cli 0.15.17 → 0.15.19

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 (46) hide show
  1. package/README.md +45 -6
  2. package/dist/assets/skills/map-your-system.md +138 -0
  3. package/dist/commands/ask.d.ts +14 -0
  4. package/dist/commands/ask.js +20 -0
  5. package/dist/commands/inbox.d.ts +65 -0
  6. package/dist/commands/inbox.js +137 -0
  7. package/dist/commands/mcp.js +88 -134
  8. package/dist/commands/pr.d.ts +37 -1
  9. package/dist/commands/pr.js +95 -77
  10. package/dist/commands/review.d.ts +24 -0
  11. package/dist/commands/review.js +193 -0
  12. package/dist/commands/schema-cmd.js +1 -1
  13. package/dist/commands/skills.js +4 -0
  14. package/dist/commands/traces.d.ts +48 -0
  15. package/dist/commands/traces.js +92 -0
  16. package/dist/commands/triage.d.ts +12 -0
  17. package/dist/commands/triage.js +90 -58
  18. package/dist/commands/webhooks.js +3 -6
  19. package/dist/index.js +79 -148
  20. package/dist/schema.d.ts +5 -2
  21. package/dist/schema.js +5 -2
  22. package/dist/utils/analysis-api.d.ts +40 -0
  23. package/dist/utils/analysis-api.js +108 -3
  24. package/dist/utils/haystack-api.d.ts +20 -0
  25. package/dist/utils/haystack-api.js +66 -0
  26. package/package.json +4 -4
  27. package/schemas/ask.v1.json +40 -0
  28. package/schemas/inbox.v1.json +27 -0
  29. package/schemas/pr.v2.json +54 -0
  30. package/schemas/traces.v1.json +53 -0
  31. package/schemas/triage.v2.json +64 -0
  32. package/dist/assets/skills/install-verification.md +0 -67
  33. package/dist/commands/verification.d.ts +0 -21
  34. package/dist/commands/verification.js +0 -291
  35. package/dist/verification/contract.d.ts +0 -240
  36. package/dist/verification/contract.js +0 -115
  37. package/dist/verification/init.d.ts +0 -10
  38. package/dist/verification/init.js +0 -243
  39. package/dist/verification/manifest.d.ts +0 -49
  40. package/dist/verification/manifest.js +0 -186
  41. package/dist/verification/runner.d.ts +0 -20
  42. package/dist/verification/runner.js +0 -102
  43. package/dist/verification/safety.d.ts +0 -10
  44. package/dist/verification/safety.js +0 -252
  45. package/dist/verification/validate.d.ts +0 -12
  46. package/dist/verification/validate.js +0 -219
package/README.md CHANGED
@@ -69,7 +69,6 @@ Create a PR from current changes. Runs pre-PR triage (code review, rules validat
69
69
 
70
70
  ```bash
71
71
  haystack submit # Triage -> create PR -> wait for analysis
72
- haystack submit --auto-fix # Discouraged alpha auto-fix for straightforward mechanical issues
73
72
  haystack submit --title "Fix auth" # Custom PR title
74
73
  haystack submit --draft # Create as draft PR
75
74
  haystack submit --force # Skip triage checks
@@ -85,8 +84,6 @@ haystack submit --review octocat # Request review from a specific teammate
85
84
 
86
85
  When `--review` is used without a username, the PR is labeled `haystack:needs-review` and appears in your team's assignment queue. When a username is provided, that person is also requested as a reviewer on GitHub.
87
86
 
88
- The `--auto-fix` flag is an alpha feature and is discouraged by default. It opts into a sandbox agent that attempts straightforward mechanical fixes before surfacing issues in the Feed. For most PRs, prefer plain `haystack submit`.
89
-
90
87
  ### `haystack triage`
91
88
 
92
89
  View Haystack analysis results for any PR. Shows the same data as the Haystack web feed: rating, verdict, structured findings with details, verified bugs, human review reasons, and agent fix prompts.
@@ -102,9 +99,51 @@ haystack triage --hook # Minimal one-liner (for session hoo
102
99
  haystack triage --clear # Clear pending submit state
103
100
  ```
104
101
 
105
- When called without a PR identifier, checks the last PR submitted via `haystack submit`. The `--hook` flag produces a single-line summary with the Haystack rating and auto-fixer status, designed for session-start hooks.
102
+ When called without a PR identifier, checks the last PR submitted via `haystack submit`. The `--hook` flag produces a single-line summary with the Haystack rating, designed for session-start hooks.
103
+
104
+ The `--json` output includes every finding and its customer-facing `suggested_fix` when available.
105
+
106
+ ### `haystack inbox list`
107
+
108
+ List PRs in your Haystack inbox with the reason each PR is present and the next action:
109
+
110
+ ```bash
111
+ haystack inbox list
112
+ haystack inbox list --json
113
+ ```
114
+
115
+ ### `haystack pr get`
116
+
117
+ Get the useful state of one PR: triage, minimal merge blockers, and customer-trace availability:
118
+
119
+ ```bash
120
+ haystack pr get 42
121
+ haystack pr get owner/repo#42 --json
122
+ ```
123
+
124
+ ### `haystack ask`
125
+
126
+ Ask Haystack Chat about a PR. Machine output contains the answer and every customer-facing source Chat consulted:
127
+
128
+ ```bash
129
+ haystack ask 42 "Why is this finding legitimate?"
130
+ haystack ask owner/repo#42 "Show the implementation" --json
131
+ haystack ask owner/repo#42 "What about its callers?" --session <session-id> --json
132
+ ```
133
+
134
+ Chat may search the complete authorized repository when the changed files are insufficient. Search results and fetched source are included in `evidence`.
135
+
136
+ ### `haystack traces`
137
+
138
+ Inspect retained customer-owned Entire checkpoints and transcript chunks:
139
+
140
+ ```bash
141
+ haystack traces list owner/repo#42 --json
142
+ haystack traces get owner/repo#42 <checkpoint-id> --json
143
+ haystack traces get owner/repo#42 <checkpoint-id> --cursor 20 --limit 20 --json
144
+ ```
106
145
 
107
- The `--json` output includes `agentFixPrompt` fields -- ready-to-paste instructions for coding agents to fix each finding.
146
+ These commands expose customer coding-session data, not Haystack's internal analysis-agent execution.
108
147
 
109
148
  ### `haystack dismiss`
110
149
 
@@ -135,7 +174,7 @@ haystack mark-reviewed acme/widgets#99 # Mark for specific repo
135
174
 
136
175
  ### `haystack pr-status`
137
176
 
138
- Show what bucket a PR is in within the Haystack pipeline (analyzing, auto-fixing, good-to-merge, issues, needs-assignment, etc.):
177
+ Show what bucket a PR is in within the Haystack pipeline (analyzing, good-to-merge, issues, needs-assignment, etc.):
139
178
 
140
179
  ```bash
141
180
  haystack pr-status 42 # Current repo, PR #42
@@ -0,0 +1,138 @@
1
+ # Map Your System for Haystack QA
2
+
3
+ Create `.haystack/system.yml` — a short file of facts about how this
4
+ repository's system works, so Haystack QA can run it.
5
+
6
+ ## Why this file exists
7
+
8
+ When Haystack reviews a risky PR, it doesn't just read the diff — it runs the
9
+ affected code in a sandbox and captures evidence. Haystack decides **what** to
10
+ test from each PR on its own. What it can't always infer is **how your system
11
+ works**: how to install and build it, how services boot, how to know they're
12
+ ready, how to seed data, how to act as a logged-in test user. That is what
13
+ this file provides. Every fact you add converts risks Haystack identified but
14
+ couldn't execute into checks that actually run.
15
+
16
+ ## Hard rules
17
+
18
+ 1. **Facts only — never test selection.** The file describes how the system
19
+ works. Keys like `scenarios`, `tests`, `skip`, or `focus` are rejected;
20
+ Haystack alone decides what to test.
21
+ 2. **Only verified facts.** Run every command yourself before declaring it.
22
+ A wrong fact is worse than a missing one — Haystack tracks which declared
23
+ facts fail and will flag them on PRs.
24
+ 3. **No secrets.** Reference environment variable *names* if needed, never
25
+ values. Anything that looks like a credential is rejected.
26
+ 4. **Personas are seeded FAKE users** reachable through dev/test-only login
27
+ paths. Never real accounts, never customer data. If a dev login helper
28
+ exists, it must be disabled in production builds.
29
+ 5. **Small and true beats big and aspirational.** Ten verified lines are more
30
+ useful than sixty guesses. Omit anything you could not verify.
31
+
32
+ ## Steps
33
+
34
+ ### 1. Inspect the repository
35
+
36
+ Determine, with evidence from the tree (lockfiles, manifests, scripts,
37
+ docker-compose, CI config, READMEs):
38
+
39
+ - package manager, install command, and any required build/codegen steps
40
+ - each runnable service: start command, port, and how "ready" is observable
41
+ (health endpoint, log line, or a command that exits 0)
42
+ - how to seed and reset local/test data
43
+ - how existing tests authenticate — is there a seeded test user or dev login?
44
+ - the test runner and any test helpers/factories generated tests should reuse
45
+ - external services the code calls, and how development runs without hitting
46
+ them for real (mock fixture, log driver, sandbox mode)
47
+ - where runtime logs are written
48
+
49
+ ### 2. Verify each fact by running it
50
+
51
+ From a clean state where possible: run the install, run the build, boot each
52
+ service and confirm its readiness signal, run the seed command, run the login
53
+ helper. Record the exact commands that worked. Drop anything you could not
54
+ make work — do not declare it with a caveat.
55
+
56
+ ### 3. Ask the developer only what you cannot determine
57
+
58
+ Typical questions worth asking a human: "Is there a dev-only way to log in as
59
+ a test user?", "Which services must be running for the API to work?", "Is
60
+ there a canonical seed script?". Keep it to the few facts you genuinely
61
+ cannot infer or verify.
62
+
63
+ ### 4. Write `.haystack/system.yml`
64
+
65
+ ```yaml
66
+ version: 1
67
+
68
+ environment:
69
+ setup:
70
+ - run: "pnpm install --frozen-lockfile"
71
+ - run: "pnpm codegen"
72
+ rationale: "generated types are imported throughout"
73
+ toolchain: { node: "20", pnpm: "10" }
74
+
75
+ services:
76
+ api:
77
+ root: "apps/api" # optional
78
+ boot: "pnpm dev"
79
+ ready: { http: "http://localhost:8080/health" } # or { log_pattern: "listening" } or { command: "pg_isready" }
80
+ env: { APP_ENV: "test" }
81
+ depends_on: [postgres]
82
+
83
+ data:
84
+ seed: "pnpm db:seed --profile=qa"
85
+ reset: "pnpm db:reset"
86
+ personas:
87
+ admin:
88
+ login: "pnpm test:login admin"
89
+ yields: "prints an Authorization header on stdout"
90
+
91
+ observability:
92
+ logs: ["apps/api/log/dev.log"]
93
+ inspect_db: "pnpm db:console --readonly"
94
+
95
+ external_services:
96
+ stripe:
97
+ offline: "repo ships a stripe-mock fixture used by tests"
98
+ must_not_touch_live: true
99
+
100
+ testing:
101
+ runner: "pnpm vitest"
102
+ probe_helpers: "tests/helpers/ has request builders and DB factories"
103
+
104
+ constraints:
105
+ - "full build takes ~6 min; apps/api boots standalone without the web build"
106
+ ```
107
+
108
+ Every section is optional — include only what you verified. All commands run
109
+ from the repo root unless a `cwd`/`root` says otherwise.
110
+
111
+ ### 5. Self-check before opening the PR
112
+
113
+ - [ ] `version: 1` is present and the file is valid YAML
114
+ - [ ] no test-selection keys (`scenarios`, `tests`, `skip`, `focus`, ...)
115
+ - [ ] no secret values anywhere (env var names only)
116
+ - [ ] every `run`/`boot`/`login`/`seed` command was executed and worked
117
+ - [ ] every service has a `ready` signal you observed
118
+ - [ ] personas are seeded fake users via dev/test-only paths
119
+
120
+ Haystack re-validates the file on its next QA run and reports precise errors
121
+ if something is wrong.
122
+
123
+ ### 6. Open a small, reviewable PR
124
+
125
+ Add only `.haystack/system.yml` (plus a dev login helper if one was genuinely
126
+ needed — dev/test-gated, with a test proving it's off in production). In the
127
+ PR description, list which facts you verified by running them and which came
128
+ from the developer's answers.
129
+
130
+ ## What happens next
131
+
132
+ Haystack treats each entry as a claim. When a QA run executes a declared
133
+ command successfully, the fact is marked **verified** with the run's receipt;
134
+ if it fails at a newer commit it is marked **broken**, and Haystack will ask
135
+ about it on a PR instead of trusting it. When QA hits something the map
136
+ doesn't cover, it asks at most a couple of targeted questions on the affected
137
+ PR, each with a ready-to-paste snippet — answering them grows this file over
138
+ time.
@@ -0,0 +1,14 @@
1
+ export interface AskOptions {
2
+ json?: boolean;
3
+ session?: string;
4
+ }
5
+ export interface AskResponse {
6
+ schema_version: string;
7
+ ref: string;
8
+ head_sha: string;
9
+ answer: string;
10
+ evidence: Array<Record<string, unknown>>;
11
+ session_id: string;
12
+ }
13
+ export declare function fetchAskPayload(ref: string, question: string, options: Pick<AskOptions, 'session'>): Promise<AskResponse>;
14
+ export declare function askHaystackCommand(ref: string, question: string, options: AskOptions): Promise<void>;
@@ -0,0 +1,20 @@
1
+ import chalk from 'chalk';
2
+ import { loadToken } from '../utils/auth.js';
3
+ import { postAskHaystack } from '../utils/haystack-api.js';
4
+ import { parsePrRef } from './pr.js';
5
+ export async function fetchAskPayload(ref, question, options) {
6
+ const pr = parsePrRef(ref);
7
+ const token = await loadToken({ owner: pr.owner, repo: pr.repo });
8
+ if (!token)
9
+ throw new Error('Not authenticated. Run `haystack login` first.');
10
+ return postAskHaystack(pr.owner, pr.repo, pr.prNumber, question, options.session, token);
11
+ }
12
+ export async function askHaystackCommand(ref, question, options) {
13
+ const response = await fetchAskPayload(ref, question, options);
14
+ if (options.json) {
15
+ console.log(JSON.stringify(response, null, 2));
16
+ return;
17
+ }
18
+ console.log(`\n${response.answer}\n`);
19
+ console.log(chalk.dim(`${response.evidence.length} source${response.evidence.length === 1 ? '' : 's'} consulted · session ${response.session_id}\n`));
20
+ }
@@ -0,0 +1,65 @@
1
+ interface GitHubPull {
2
+ number: number;
3
+ title: string;
4
+ html_url: string;
5
+ draft?: boolean;
6
+ user: {
7
+ id: number;
8
+ login: string;
9
+ };
10
+ assignees?: Array<{
11
+ id: number;
12
+ login: string;
13
+ }>;
14
+ requested_reviewers?: Array<{
15
+ id: number;
16
+ login: string;
17
+ }>;
18
+ head: {
19
+ sha: string;
20
+ ref: string;
21
+ };
22
+ base: {
23
+ ref: string;
24
+ };
25
+ }
26
+ interface InboxCandidate extends GitHubPull {
27
+ owner: string;
28
+ repo: string;
29
+ }
30
+ interface AnalysisSummary {
31
+ analysisStatus?: 'pending' | 'in_queue' | 'ready' | 'error';
32
+ analysisVerdict?: 'clean' | 'has-issues' | 'needs-review';
33
+ haystackRating?: number;
34
+ needsHumanReview?: boolean;
35
+ findingsCount?: number;
36
+ merge?: {
37
+ status: string;
38
+ reason: string;
39
+ blocking_checks: string[];
40
+ action: string | null;
41
+ };
42
+ }
43
+ export interface InboxListOptions {
44
+ json?: boolean;
45
+ }
46
+ export declare function isRelevantToUser(pr: GitHubPull, userId: number): boolean;
47
+ export declare function classify(candidate: InboxCandidate, analysis: AnalysisSummary | undefined): {
48
+ bucket: string;
49
+ reason: string;
50
+ action: string;
51
+ };
52
+ export declare function fetchInboxPayload(token: string): Promise<{
53
+ pull_requests: {
54
+ bucket: string;
55
+ reason: string;
56
+ action: string;
57
+ ref: string;
58
+ title: string;
59
+ head_sha: string;
60
+ }[];
61
+ } & {
62
+ schema_version: string;
63
+ }>;
64
+ export declare function inboxListCommand(options: InboxListOptions): Promise<void>;
65
+ export {};
@@ -0,0 +1,137 @@
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
+ }