@haystackeditor/cli 0.15.19 → 0.15.21
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 +100 -1
- package/dist/assets/skills/map-your-system.md +9 -4
- package/dist/commands/ask.js +2 -2
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.js +46 -14
- package/dist/commands/dismiss.d.ts +15 -3
- package/dist/commands/dismiss.js +66 -116
- package/dist/commands/mcp.js +96 -1
- package/dist/commands/policy.js +31 -12
- package/dist/commands/pr-status.d.ts +125 -2
- package/dist/commands/pr-status.js +88 -73
- package/dist/commands/pr.d.ts +1 -7
- package/dist/commands/pr.js +10 -21
- package/dist/commands/request-review.d.ts +12 -1
- package/dist/commands/request-review.js +42 -65
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.js +58 -60
- package/dist/commands/setup.js +4 -1
- package/dist/commands/submit.d.ts +3 -0
- package/dist/commands/submit.js +178 -38
- package/dist/commands/system-map.d.ts +42 -0
- package/dist/commands/system-map.js +228 -0
- package/dist/commands/tokens.js +3 -2
- package/dist/commands/traces.js +2 -2
- package/dist/commands/triage.d.ts +1 -6
- package/dist/commands/triage.js +51 -55
- package/dist/commands/verify-core.d.ts +424 -0
- package/dist/commands/verify-core.js +753 -0
- package/dist/commands/verify-mcp.d.ts +8 -0
- package/dist/commands/verify-mcp.js +458 -0
- package/dist/commands/verify-ops.d.ts +158 -0
- package/dist/commands/verify-ops.js +1148 -0
- package/dist/commands/verify-reseal.d.ts +9 -0
- package/dist/commands/verify-reseal.js +148 -0
- package/dist/commands/verify-sandboxes.d.ts +95 -0
- package/dist/commands/verify-sandboxes.js +352 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.js +97 -0
- package/dist/index.js +343 -20
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +4 -0
- package/dist/states.d.ts +29 -0
- package/dist/states.js +46 -0
- package/dist/triage/runner.d.ts +7 -0
- package/dist/triage/runner.js +2 -2
- package/dist/types.d.ts +54 -0
- package/dist/types.js +15 -0
- package/dist/utils/action-output.d.ts +24 -0
- package/dist/utils/action-output.js +26 -0
- package/dist/utils/analysis-api.d.ts +11 -0
- package/dist/utils/analysis-api.js +29 -3
- package/dist/utils/auth.js +7 -0
- package/dist/utils/git.d.ts +4 -1
- package/dist/utils/git.js +17 -7
- package/dist/utils/haystack-api.d.ts +15 -0
- package/dist/utils/haystack-api.js +58 -7
- package/dist/utils/pr-ref.d.ts +27 -0
- package/dist/utils/pr-ref.js +56 -0
- package/package.json +2 -2
- package/schemas/action.v1.json +22 -0
- package/schemas/error.v1.json +14 -0
- package/schemas/pr-status.v1.json +61 -0
- package/schemas/pr.v1.json +80 -17
- package/schemas/setup.v1.json +66 -12
- package/schemas/submit.v1.json +88 -0
- package/schemas/triage.v1.json +71 -18
package/README.md
CHANGED
|
@@ -21,6 +21,74 @@ The `setup` command walks you through an interactive wizard:
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## For AI agents: the machine-readable contract
|
|
25
|
+
|
|
26
|
+
Most consumers of this CLI are coding agents. These invariants hold everywhere:
|
|
27
|
+
|
|
28
|
+
- **`--json` means pure stdout.** When `--json` is passed, stdout carries exactly
|
|
29
|
+
one JSON document (NDJSON stream for `setup --json`); all progress, spinners,
|
|
30
|
+
and prose go to stderr. On failure, stdout still gets a versioned error
|
|
31
|
+
envelope (`haystack schema error`: `{"schema_version": "1.0.0", "status":
|
|
32
|
+
"error", "error": "..."}`) and the process exits 1.
|
|
33
|
+
- **Every JSON payload is versioned and schema'd.** Payloads carry
|
|
34
|
+
`schema_version`; print the contract with `haystack schema <name>`
|
|
35
|
+
(`haystack schema` lists all: `triage`, `pr`, `pr-status`, `inbox`, `ask`,
|
|
36
|
+
`traces`, `submit`, `action`, `error`, `setup`). Schemas are JSON-Schema
|
|
37
|
+
2020-12 and CI-guarded against drift.
|
|
38
|
+
- **One state vocabulary.** All state tokens are snake_case across every
|
|
39
|
+
command: verdicts are `good_to_merge` / `needs_review` / `needs_input`;
|
|
40
|
+
feed buckets are `analyzing`, `good_to_merge`, `issues`, `needs_assignment`,
|
|
41
|
+
`needs_shepherding`, etc. No command emits kebab-case states.
|
|
42
|
+
- **PR refs are uniform.** Every command taking a PR accepts `123`, `#123`,
|
|
43
|
+
`owner/repo#123`, or a GitHub PR URL. Bare numbers infer the repo from the
|
|
44
|
+
`origin` remote.
|
|
45
|
+
- **Exit codes**: 0 = the requested action happened (a created PR with findings
|
|
46
|
+
is still 0 — read the verdict from the JSON); 1 = it did not. Commands never
|
|
47
|
+
print an error and exit 0.
|
|
48
|
+
- **Waiting is bounded and skippable.** `submit` waits up to 10 min for
|
|
49
|
+
analysis, `triage` polls up to 5 min, `review` up to 25 min; every waiting
|
|
50
|
+
command takes `--no-wait`. Non-terminal outcomes include a `next` field with
|
|
51
|
+
the follow-up command (usually `haystack triage <ref> --json`).
|
|
52
|
+
- **"review" disambiguation**: `haystack review` re-runs the *machine*
|
|
53
|
+
analysis. For *human* review use `haystack submit --review` or
|
|
54
|
+
`haystack request-review`.
|
|
55
|
+
|
|
56
|
+
**The agent workflow for submitting:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
haystack submit --json # emits the submit payload incl. ref + analysis verdict
|
|
60
|
+
haystack triage <ref> --json # poll findings later / after --no-wait
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**`haystack mcp`** runs a stdio MCP server exposing the same payloads as tools:
|
|
64
|
+
`inbox_list`, `pr_get`, `pr_status`, `triage_get`, `ask_haystack`,
|
|
65
|
+
`traces_list`, `traces_get`, `dismiss`, `mark_reviewed`, `undismiss`,
|
|
66
|
+
`request_review`, `trigger_review`, and `schema`.
|
|
67
|
+
|
|
68
|
+
**`haystack verify mcp`** runs the cloud-verifier MCP server. Retained
|
|
69
|
+
application state is available through `verify_reopen`, `verify_refork`, and
|
|
70
|
+
`verify_cleanup`. Agents select state by an exact Archil fork ID or a stable
|
|
71
|
+
run/test/side coordinate; refork returns a durable child ID that can be
|
|
72
|
+
reopened or nested again without receiving provider credentials. The matching
|
|
73
|
+
CLI flow is:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
haystack verify reopen latest 3
|
|
77
|
+
# inspect or mutate the returned sandbox
|
|
78
|
+
haystack verify refork latest 3 attempt:fix-2
|
|
79
|
+
haystack verify reopen archil:<disk-id>:branch:<returned-child>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**`haystack setup --json`** speaks NDJSON: events (`question`, `permission`,
|
|
83
|
+
`progress`, `result`) on stdout, replies on stdin keyed by
|
|
84
|
+
`requestID`/`permissionID`. `haystack schema setup` documents the full
|
|
85
|
+
protocol, including the reply shapes. Pre-supply answers with `--repo`,
|
|
86
|
+
`--yes`, `--no-auto-merge`, or `--answers <file>` to skip questions. Note:
|
|
87
|
+
`--yes` enables auto-merge (the wizard default) — pass `--no-auto-merge` to
|
|
88
|
+
opt out.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
24
92
|
## CLI Commands
|
|
25
93
|
|
|
26
94
|
### `haystack setup`
|
|
@@ -69,12 +137,18 @@ Create a PR from current changes. Runs pre-PR triage (code review, rules validat
|
|
|
69
137
|
|
|
70
138
|
```bash
|
|
71
139
|
haystack submit # Triage -> create PR -> wait for analysis
|
|
140
|
+
haystack submit --json # Agent mode: one JSON doc on stdout, progress on stderr
|
|
72
141
|
haystack submit --title "Fix auth" # Custom PR title
|
|
73
142
|
haystack submit --draft # Create as draft PR
|
|
74
143
|
haystack submit --force # Skip triage checks
|
|
75
144
|
haystack submit --no-wait # Don't wait for analysis results
|
|
76
145
|
```
|
|
77
146
|
|
|
147
|
+
The `--json` payload (`haystack schema submit`) reports the PR ref, the
|
|
148
|
+
resolved title/body and where each came from, auto-merge/auto-fix state with
|
|
149
|
+
its source (flag vs `.haystack.json`), and the analysis outcome in the shared
|
|
150
|
+
verdict vocabulary.
|
|
151
|
+
|
|
78
152
|
**Review routing**: By default, PRs go to the auto-merge queue -- if analysis passes, the PR is merged automatically. Use `--review` to route it for human review instead:
|
|
79
153
|
|
|
80
154
|
```bash
|
|
@@ -145,6 +219,26 @@ haystack traces get owner/repo#42 <checkpoint-id> --cursor 20 --limit 20 --json
|
|
|
145
219
|
|
|
146
220
|
These commands expose customer coding-session data, not Haystack's internal analysis-agent execution.
|
|
147
221
|
|
|
222
|
+
### `haystack review`
|
|
223
|
+
|
|
224
|
+
Trigger a fresh *machine* analysis of a PR's current head (Haystack analyzes once at PR open; later pushes only get a resolution check). For requesting *human* review, use `haystack request-review`.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
haystack review 42 # Re-analyze and wait for the result
|
|
228
|
+
haystack review 42 --no-wait # Trigger and exit
|
|
229
|
+
haystack review 42 --json # Machine-readable outcome (schema: action)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `haystack request-review`
|
|
233
|
+
|
|
234
|
+
Tag a PR as needing human review (adds `haystack:needs-review`), optionally requesting a specific GitHub user:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
haystack request-review 42 # Into the needs-assignment queue
|
|
238
|
+
haystack request-review 42 octocat # Also request review from octocat
|
|
239
|
+
haystack request-review 42 --json
|
|
240
|
+
```
|
|
241
|
+
|
|
148
242
|
### `haystack dismiss`
|
|
149
243
|
|
|
150
244
|
Dismiss analysis findings for a PR, moving it from "Issues Found" to "Good to Merge" in the feed. The override is tied to the PR's current HEAD commit.
|
|
@@ -152,6 +246,7 @@ Dismiss analysis findings for a PR, moving it from "Issues Found" to "Good to Me
|
|
|
152
246
|
```bash
|
|
153
247
|
haystack dismiss 42 # Dismiss findings for PR #42
|
|
154
248
|
haystack dismiss acme/widgets#99 # Dismiss for specific repo
|
|
249
|
+
haystack dismiss 42 --json # Machine-readable (schema: action)
|
|
155
250
|
```
|
|
156
251
|
|
|
157
252
|
### `haystack undismiss`
|
|
@@ -205,10 +300,14 @@ haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
|
|
|
205
300
|
haystack config wait-for-reviewers add cursor coderabbit # Add multiple
|
|
206
301
|
haystack config wait-for-reviewers remove cursor # Stop waiting
|
|
207
302
|
haystack config wait-for-reviewers clear # Wait for none
|
|
208
|
-
# Also accepts raw GitHub bot usernames:
|
|
303
|
+
# Also accepts raw GitHub bot usernames (must end in [bot]):
|
|
209
304
|
haystack config wait-for-reviewers add cursor-bugbot[bot]
|
|
210
305
|
```
|
|
211
306
|
|
|
307
|
+
Reviewer names are validated: anything that is neither a known friendly name
|
|
308
|
+
nor a `...[bot]` username is rejected (a stored typo would make the merge
|
|
309
|
+
queue wait forever for a bot that can't post).
|
|
310
|
+
|
|
212
311
|
### `haystack skills`
|
|
213
312
|
|
|
214
313
|
Manage AI skills for your coding CLI:
|
|
@@ -108,11 +108,16 @@ constraints:
|
|
|
108
108
|
Every section is optional — include only what you verified. All commands run
|
|
109
109
|
from the repo root unless a `cwd`/`root` says otherwise.
|
|
110
110
|
|
|
111
|
-
### 5.
|
|
111
|
+
### 5. Validate before opening the PR
|
|
112
|
+
|
|
113
|
+
Run the validator — it applies the same rules Haystack QA applies:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx @haystackeditor/cli system-map validate
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Fix every reported error, then confirm the things the validator cannot check:
|
|
112
120
|
|
|
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
121
|
- [ ] every `run`/`boot`/`login`/`seed` command was executed and worked
|
|
117
122
|
- [ ] every service has a `ready` signal you observed
|
|
118
123
|
- [ ] personas are seeded fake users via dev/test-only paths
|
package/dist/commands/ask.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadToken } from '../utils/auth.js';
|
|
3
3
|
import { postAskHaystack } from '../utils/haystack-api.js';
|
|
4
|
-
import { parsePrRef } from '
|
|
4
|
+
import { parsePrRef } from '../utils/pr-ref.js';
|
|
5
5
|
export async function fetchAskPayload(ref, question, options) {
|
|
6
|
-
const pr = parsePrRef(ref);
|
|
6
|
+
const pr = parsePrRef(ref, 'ask');
|
|
7
7
|
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
8
8
|
if (!token)
|
|
9
9
|
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
@@ -33,5 +33,14 @@ export declare function getAutoFixStatus(): Promise<void>;
|
|
|
33
33
|
export declare function enableAutoFix(): Promise<void>;
|
|
34
34
|
export declare function disableAutoFix(): Promise<void>;
|
|
35
35
|
export declare function handleAutoFix(action?: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a source name (e.g. "cursor") to its GitHub bot username. Raw bot
|
|
38
|
+
* usernames (anything ending in `[bot]`) pass through unchanged.
|
|
39
|
+
*
|
|
40
|
+
* Unknown names are REJECTED, not stored: this value must exactly match a
|
|
41
|
+
* GitHub bot login or the merge queue waits forever for a reviewer that can
|
|
42
|
+
* never post. (A typo used to be saved verbatim and reported as success.)
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveToBot(input: string): string;
|
|
36
45
|
export declare function handleWaitForReviewers(action?: string, reviewers?: string[]): Promise<void>;
|
|
37
46
|
export {};
|
package/dist/commands/config.js
CHANGED
|
@@ -411,14 +411,32 @@ function printAvailableReviewers(currentBots) {
|
|
|
411
411
|
}
|
|
412
412
|
console.log();
|
|
413
413
|
}
|
|
414
|
-
/**
|
|
415
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Resolve a source name (e.g. "cursor") to its GitHub bot username. Raw bot
|
|
416
|
+
* usernames (anything ending in `[bot]`) pass through unchanged.
|
|
417
|
+
*
|
|
418
|
+
* Unknown names are REJECTED, not stored: this value must exactly match a
|
|
419
|
+
* GitHub bot login or the merge queue waits forever for a reviewer that can
|
|
420
|
+
* never post. (A typo used to be saved verbatim and reported as success.)
|
|
421
|
+
*/
|
|
422
|
+
export function resolveToBot(input) {
|
|
423
|
+
const lower = input.toLowerCase();
|
|
424
|
+
if (AI_REVIEWER_SOURCES.includes(lower)) {
|
|
425
|
+
return AI_REVIEWER_BOT_USERNAMES[lower];
|
|
426
|
+
}
|
|
427
|
+
if (input.endsWith('[bot]')) {
|
|
428
|
+
return input;
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`Unknown reviewer: "${input}".\n\n` +
|
|
431
|
+
`Use a friendly name (${AI_REVIEWER_SOURCES.join(', ')})\n` +
|
|
432
|
+
`or an exact GitHub bot username ending in [bot] (e.g. cursor-bugbot[bot]).`);
|
|
433
|
+
}
|
|
434
|
+
/** Like resolveToBot, but unknown names pass through verbatim (for `remove`). */
|
|
435
|
+
function resolveToBotLenient(input) {
|
|
416
436
|
const lower = input.toLowerCase();
|
|
417
|
-
// Check if it's a known friendly source name
|
|
418
437
|
if (AI_REVIEWER_SOURCES.includes(lower)) {
|
|
419
438
|
return AI_REVIEWER_BOT_USERNAMES[lower];
|
|
420
439
|
}
|
|
421
|
-
// Already a bot username (e.g. "cursor-bugbot[bot]")
|
|
422
440
|
return input;
|
|
423
441
|
}
|
|
424
442
|
function displayName(botUsername) {
|
|
@@ -463,7 +481,15 @@ export async function handleWaitForReviewers(action, reviewers) {
|
|
|
463
481
|
process.exit(1);
|
|
464
482
|
return; // unreachable — satisfies TS narrowing
|
|
465
483
|
}
|
|
466
|
-
|
|
484
|
+
let resolved;
|
|
485
|
+
try {
|
|
486
|
+
resolved = reviewers.map(resolveToBot);
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
490
|
+
process.exit(1);
|
|
491
|
+
return; // unreachable — satisfies TS narrowing
|
|
492
|
+
}
|
|
467
493
|
const { config, path, bots: current } = getMergeQueueConfig();
|
|
468
494
|
const merged = Array.from(new Set([...current, ...resolved]));
|
|
469
495
|
saveMergeQueueBots(config, path, merged);
|
|
@@ -483,7 +509,10 @@ export async function handleWaitForReviewers(action, reviewers) {
|
|
|
483
509
|
process.exit(1);
|
|
484
510
|
return; // unreachable — satisfies TS narrowing
|
|
485
511
|
}
|
|
486
|
-
|
|
512
|
+
// Lenient on remove: stored entries may predate strict validation
|
|
513
|
+
// (arbitrary strings used to be saved verbatim), and removing them must
|
|
514
|
+
// stay possible.
|
|
515
|
+
const resolved = reviewers.map(resolveToBotLenient);
|
|
487
516
|
const { config, path, bots: current } = getMergeQueueConfig();
|
|
488
517
|
const removeSet = new Set(resolved);
|
|
489
518
|
const remaining = current.filter(b => !removeSet.has(b));
|
|
@@ -506,12 +535,15 @@ export async function handleWaitForReviewers(action, reviewers) {
|
|
|
506
535
|
console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
|
|
507
536
|
return;
|
|
508
537
|
}
|
|
509
|
-
// Unknown action —
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
console.log(
|
|
516
|
-
console.log(
|
|
538
|
+
// Unknown action — reject it. This used to fall through to an implicit
|
|
539
|
+
// "add", so a typo'd action (`wait-for-reviewers ad cursor`) silently
|
|
540
|
+
// stored a bogus bot named "ad" and reported success.
|
|
541
|
+
console.error(chalk.red(`\nUnknown action: "${action}".`));
|
|
542
|
+
console.log('\nActions:');
|
|
543
|
+
console.log(' list Show configured reviewer bots (default)');
|
|
544
|
+
console.log(' add <names> Add one or more reviewer bots');
|
|
545
|
+
console.log(' remove <names> Remove one or more reviewer bots');
|
|
546
|
+
console.log(' clear Remove all reviewer bots');
|
|
547
|
+
console.log(chalk.dim('\nExample: haystack config wait-for-reviewers add cursor\n'));
|
|
548
|
+
process.exit(1);
|
|
517
549
|
}
|
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
* owner/repo#123 # Fully qualified
|
|
13
13
|
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import { type ActionPayload } from '../utils/action-output.js';
|
|
16
|
+
type OverrideType = 'findings-dismissed' | 'review-not-needed';
|
|
17
|
+
/** Post a user override and return the action payload. Throws on failure. */
|
|
18
|
+
export declare function performOverride(identifier: string, type: OverrideType, commandName: string): Promise<ActionPayload & {
|
|
19
|
+
head_sha: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** Clear all overrides and return the action payload. Throws on failure. */
|
|
22
|
+
export declare function performUndismiss(identifier: string): Promise<ActionPayload>;
|
|
23
|
+
export interface OverrideCommandOptions {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function dismissCommand(identifier: string, options?: OverrideCommandOptions): Promise<void>;
|
|
27
|
+
export declare function markReviewedCommand(identifier: string, options?: OverrideCommandOptions): Promise<void>;
|
|
28
|
+
export declare function undismissCommand(identifier: string, options?: OverrideCommandOptions): Promise<void>;
|
|
29
|
+
export {};
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -13,38 +13,10 @@
|
|
|
13
13
|
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
14
14
|
*/
|
|
15
15
|
import chalk from 'chalk';
|
|
16
|
+
import { emitActionPayload, failAction } from '../utils/action-output.js';
|
|
16
17
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
// PR identifier parsing (shared with triage — duplicated to avoid circular deps)
|
|
20
|
-
// ============================================================================
|
|
21
|
-
function parsePRIdentifier(identifier, commandName) {
|
|
22
|
-
const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
|
|
23
|
-
if (urlMatch) {
|
|
24
|
-
return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
|
|
25
|
-
}
|
|
26
|
-
const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
27
|
-
if (qualifiedMatch) {
|
|
28
|
-
return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
|
|
29
|
-
}
|
|
30
|
-
const numberMatch = identifier.match(/^#?(\d+)$/);
|
|
31
|
-
if (numberMatch) {
|
|
32
|
-
const prNumber = parseInt(numberMatch[1], 10);
|
|
33
|
-
try {
|
|
34
|
-
const remote = parseRemoteUrl('origin');
|
|
35
|
-
return { owner: remote.owner, repo: remote.repo, prNumber };
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
|
|
39
|
-
`Use the full format: haystack ${commandName} owner/repo#${prNumber}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
|
|
43
|
-
`Accepted formats:\n` +
|
|
44
|
-
` haystack ${commandName} 123\n` +
|
|
45
|
-
` haystack ${commandName} owner/repo#123\n` +
|
|
46
|
-
` haystack ${commandName} https://github.com/owner/repo/pull/123`);
|
|
47
|
-
}
|
|
18
|
+
import { classifyHttpError } from '../utils/haystack-api.js';
|
|
19
|
+
import { parsePrRef } from '../utils/pr-ref.js';
|
|
48
20
|
// ============================================================================
|
|
49
21
|
// GitHub API helpers
|
|
50
22
|
// ============================================================================
|
|
@@ -60,7 +32,7 @@ async function getPRHeadSha(owner, repo, prNumber, token) {
|
|
|
60
32
|
if (response.status === 404) {
|
|
61
33
|
throw new Error(`PR #${prNumber} not found in ${owner}/${repo}`);
|
|
62
34
|
}
|
|
63
|
-
throw
|
|
35
|
+
throw await classifyHttpError(response, `Fetching PR #${prNumber}`);
|
|
64
36
|
}
|
|
65
37
|
const data = await response.json();
|
|
66
38
|
return data.head.sha;
|
|
@@ -80,14 +52,7 @@ async function postUserOverride(repoFullName, prNumber, commitSha, type, token)
|
|
|
80
52
|
body: JSON.stringify({ repoFullName, prNumber, commitSha, type }),
|
|
81
53
|
});
|
|
82
54
|
if (!response.ok) {
|
|
83
|
-
|
|
84
|
-
if (response.status === 401) {
|
|
85
|
-
throw new Error('Authentication failed. Run `haystack login` to re-authenticate.');
|
|
86
|
-
}
|
|
87
|
-
if (response.status === 403) {
|
|
88
|
-
throw new Error('Access denied — you may not have permission to this repository.');
|
|
89
|
-
}
|
|
90
|
-
throw new Error(`API error (${response.status}): ${body}`);
|
|
55
|
+
throw await classifyHttpError(response, 'User override');
|
|
91
56
|
}
|
|
92
57
|
}
|
|
93
58
|
async function deleteUserOverrides(repoFullName, prNumber, token) {
|
|
@@ -101,108 +66,93 @@ async function deleteUserOverrides(repoFullName, prNumber, token) {
|
|
|
101
66
|
body: JSON.stringify({ repoFullName, prNumber }),
|
|
102
67
|
});
|
|
103
68
|
if (!response.ok) {
|
|
104
|
-
|
|
105
|
-
if (response.status === 401) {
|
|
106
|
-
throw new Error('Authentication failed. Run `haystack login` to re-authenticate.');
|
|
107
|
-
}
|
|
108
|
-
if (response.status === 403) {
|
|
109
|
-
throw new Error('Access denied — you may not have permission to this repository.');
|
|
110
|
-
}
|
|
111
|
-
throw new Error(`API error (${response.status}): ${body}`);
|
|
69
|
+
throw await classifyHttpError(response, 'Clearing user overrides');
|
|
112
70
|
}
|
|
113
71
|
}
|
|
114
72
|
// ============================================================================
|
|
115
|
-
//
|
|
73
|
+
// Pure payload functions (shared by the CLI commands and the MCP server —
|
|
74
|
+
// they throw instead of exiting, and never write to stdout)
|
|
116
75
|
// ============================================================================
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
pr = parsePRIdentifier(identifier, commandName);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
console.error(chalk.red(`\n${err.message}\n`));
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
// Require auth
|
|
128
|
-
let authContext;
|
|
129
|
-
try {
|
|
130
|
-
authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
console.error(chalk.red(`\n${err.message}\n`));
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
76
|
+
/** Post a user override and return the action payload. Throws on failure. */
|
|
77
|
+
export async function performOverride(identifier, type, commandName) {
|
|
78
|
+
const pr = parsePrRef(identifier, commandName);
|
|
79
|
+
const authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
137
80
|
const repoFullName = `${pr.owner}/${pr.repo}`;
|
|
81
|
+
const headSha = await getPRHeadSha(pr.owner, pr.repo, pr.prNumber, authContext.token);
|
|
82
|
+
await postUserOverride(repoFullName, pr.prNumber, headSha, type, authContext.token);
|
|
83
|
+
return {
|
|
84
|
+
action: type === 'findings-dismissed' ? 'dismiss' : 'mark_reviewed',
|
|
85
|
+
status: 'ok',
|
|
86
|
+
ref: `${repoFullName}#${pr.prNumber}`,
|
|
87
|
+
head_sha: headSha,
|
|
88
|
+
detail: type === 'findings-dismissed'
|
|
89
|
+
? 'Findings dismissed; the PR moves to "Good to Merge". A new commit with net-new issues resurfaces it.'
|
|
90
|
+
: 'Human review marked as not needed; the PR moves to "Good to Merge". A new commit invalidates the override.',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Clear all overrides and return the action payload. Throws on failure. */
|
|
94
|
+
export async function performUndismiss(identifier) {
|
|
95
|
+
const pr = parsePrRef(identifier, 'undismiss');
|
|
96
|
+
const authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
97
|
+
const repoFullName = `${pr.owner}/${pr.repo}`;
|
|
98
|
+
await deleteUserOverrides(repoFullName, pr.prNumber, authContext.token);
|
|
99
|
+
return {
|
|
100
|
+
action: 'undismiss',
|
|
101
|
+
status: 'ok',
|
|
102
|
+
ref: `${repoFullName}#${pr.prNumber}`,
|
|
103
|
+
detail: 'All overrides cleared; the PR returns to its original feed bucket.',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function runOverride(identifier, type, commandName, options) {
|
|
107
|
+
const action = type === 'findings-dismissed' ? 'dismiss' : 'mark_reviewed';
|
|
138
108
|
const actionLabel = type === 'findings-dismissed'
|
|
139
109
|
? 'Dismissing findings'
|
|
140
110
|
: 'Marking review as not needed';
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
let
|
|
111
|
+
if (!options.json)
|
|
112
|
+
console.log(chalk.dim(`\n${actionLabel} for ${identifier}...`));
|
|
113
|
+
let payload;
|
|
144
114
|
try {
|
|
145
|
-
|
|
115
|
+
payload = await performOverride(identifier, type, commandName);
|
|
146
116
|
}
|
|
147
117
|
catch (err) {
|
|
148
|
-
|
|
149
|
-
|
|
118
|
+
failAction(options.json, action, null, err.message);
|
|
119
|
+
return; // unreachable — satisfies TS narrowing
|
|
150
120
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
console.error(chalk.red(`\n${err.message}\n`));
|
|
157
|
-
process.exit(1);
|
|
121
|
+
if (options.json) {
|
|
122
|
+
emitActionPayload(payload);
|
|
123
|
+
return;
|
|
158
124
|
}
|
|
159
|
-
// Success
|
|
160
125
|
if (type === 'findings-dismissed') {
|
|
161
|
-
console.log(chalk.green(`\n Findings dismissed for ${
|
|
126
|
+
console.log(chalk.green(`\n Findings dismissed for ${payload.ref}`));
|
|
162
127
|
}
|
|
163
128
|
else {
|
|
164
|
-
console.log(chalk.green(`\n Review marked as not needed for ${
|
|
129
|
+
console.log(chalk.green(`\n Review marked as not needed for ${payload.ref}`));
|
|
165
130
|
}
|
|
166
|
-
console.log(chalk.dim(` Commit: ${
|
|
131
|
+
console.log(chalk.dim(` Commit: ${payload.head_sha.slice(0, 7)}`));
|
|
167
132
|
console.log(chalk.dim(' The PR will move to "Good to Merge" in the feed.'));
|
|
168
133
|
console.log(chalk.dim(' If a new commit introduces net-new issues, the PR will resurface.\n'));
|
|
169
134
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// ============================================================================
|
|
173
|
-
export async function dismissCommand(identifier) {
|
|
174
|
-
await runOverride(identifier, 'findings-dismissed', 'dismiss');
|
|
135
|
+
export async function dismissCommand(identifier, options = {}) {
|
|
136
|
+
await runOverride(identifier, 'findings-dismissed', 'dismiss', options);
|
|
175
137
|
}
|
|
176
|
-
export async function markReviewedCommand(identifier) {
|
|
177
|
-
await runOverride(identifier, 'review-not-needed', 'mark-reviewed');
|
|
138
|
+
export async function markReviewedCommand(identifier, options = {}) {
|
|
139
|
+
await runOverride(identifier, 'review-not-needed', 'mark-reviewed', options);
|
|
178
140
|
}
|
|
179
|
-
export async function undismissCommand(identifier) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
console.error(chalk.red(`\n${err.message}\n`));
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
let authContext;
|
|
141
|
+
export async function undismissCommand(identifier, options = {}) {
|
|
142
|
+
if (!options.json)
|
|
143
|
+
console.log(chalk.dim(`\nClearing overrides for ${identifier}...`));
|
|
144
|
+
let payload;
|
|
189
145
|
try {
|
|
190
|
-
|
|
146
|
+
payload = await performUndismiss(identifier);
|
|
191
147
|
}
|
|
192
148
|
catch (err) {
|
|
193
|
-
|
|
194
|
-
|
|
149
|
+
failAction(options.json, 'undismiss', null, err.message);
|
|
150
|
+
return; // unreachable — satisfies TS narrowing
|
|
195
151
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
await deleteUserOverrides(repoFullName, pr.prNumber, authContext.token);
|
|
201
|
-
}
|
|
202
|
-
catch (err) {
|
|
203
|
-
console.error(chalk.red(`\n${err.message}\n`));
|
|
204
|
-
process.exit(1);
|
|
152
|
+
if (options.json) {
|
|
153
|
+
emitActionPayload(payload);
|
|
154
|
+
return;
|
|
205
155
|
}
|
|
206
|
-
console.log(chalk.green(`\n Overrides cleared for ${
|
|
156
|
+
console.log(chalk.green(`\n Overrides cleared for ${payload.ref}`));
|
|
207
157
|
console.log(chalk.dim(' The PR will return to its original feed bucket.\n'));
|
|
208
158
|
}
|