@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/dist/states.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical machine-readable PR-state vocabulary for every CLI JSON surface.
|
|
3
|
+
*
|
|
4
|
+
* One concept, one spelling: all `--json` output uses snake_case state tokens.
|
|
5
|
+
* Before this module existed the same state appeared as `good_to_merge`
|
|
6
|
+
* (triage/pr get/inbox) and `good-to-merge` (pr-status, raw server buckets),
|
|
7
|
+
* so no single parser worked across commands. Server responses that use
|
|
8
|
+
* kebab-case are normalized at the edge via `normalizeFeedBucket`.
|
|
9
|
+
*/
|
|
10
|
+
/** Public triage verdict — emitted by `triage`, `pr get`, `submit`, `pr-status`. */
|
|
11
|
+
export declare const TRIAGE_VERDICTS: readonly ["good_to_merge", "needs_review", "needs_input"];
|
|
12
|
+
export type TriageVerdict = (typeof TRIAGE_VERDICTS)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Map the server's internal analysis verdict ('clean' | 'has-issues' |
|
|
15
|
+
* 'needs-review', with a rating-based fallback for pre-verdict analyses) to
|
|
16
|
+
* the public triage verdict. This mapping used to be duplicated inline in
|
|
17
|
+
* triage.ts and pr.ts.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toPublicVerdict(analysisVerdict: string | null | undefined, haystackRating?: number | null): TriageVerdict;
|
|
20
|
+
/**
|
|
21
|
+
* Known feed buckets in canonical snake_case form. The server may introduce
|
|
22
|
+
* new buckets; `normalizeFeedBucket` converts mechanically so unknown buckets
|
|
23
|
+
* pass through in the same convention instead of crashing or leaking kebab-case.
|
|
24
|
+
*/
|
|
25
|
+
export declare const FEED_BUCKETS: readonly ["analyzing", "auto_fixing", "good_to_merge", "failed_when_merging", "issues", "needs_assignment", "needs_shepherding", "needs_fixing", "awaiting_review", "review_requested", "snoozed"];
|
|
26
|
+
export type FeedBucket = (typeof FEED_BUCKETS)[number];
|
|
27
|
+
/** Normalize a server feed bucket (kebab-case) to the canonical snake_case form. */
|
|
28
|
+
export declare function normalizeFeedBucket(bucket: string): string;
|
|
29
|
+
export declare function normalizeFeedBucket(bucket: string | null | undefined): string | null;
|
package/dist/states.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical machine-readable PR-state vocabulary for every CLI JSON surface.
|
|
3
|
+
*
|
|
4
|
+
* One concept, one spelling: all `--json` output uses snake_case state tokens.
|
|
5
|
+
* Before this module existed the same state appeared as `good_to_merge`
|
|
6
|
+
* (triage/pr get/inbox) and `good-to-merge` (pr-status, raw server buckets),
|
|
7
|
+
* so no single parser worked across commands. Server responses that use
|
|
8
|
+
* kebab-case are normalized at the edge via `normalizeFeedBucket`.
|
|
9
|
+
*/
|
|
10
|
+
/** Public triage verdict — emitted by `triage`, `pr get`, `submit`, `pr-status`. */
|
|
11
|
+
export const TRIAGE_VERDICTS = ['good_to_merge', 'needs_review', 'needs_input'];
|
|
12
|
+
/**
|
|
13
|
+
* Map the server's internal analysis verdict ('clean' | 'has-issues' |
|
|
14
|
+
* 'needs-review', with a rating-based fallback for pre-verdict analyses) to
|
|
15
|
+
* the public triage verdict. This mapping used to be duplicated inline in
|
|
16
|
+
* triage.ts and pr.ts.
|
|
17
|
+
*/
|
|
18
|
+
export function toPublicVerdict(analysisVerdict, haystackRating) {
|
|
19
|
+
const verdict = analysisVerdict ?? ((haystackRating ?? 0) >= 5 ? 'clean' : 'has-issues');
|
|
20
|
+
if (verdict === 'clean')
|
|
21
|
+
return 'good_to_merge';
|
|
22
|
+
if (verdict === 'needs-review')
|
|
23
|
+
return 'needs_review';
|
|
24
|
+
return 'needs_input';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Known feed buckets in canonical snake_case form. The server may introduce
|
|
28
|
+
* new buckets; `normalizeFeedBucket` converts mechanically so unknown buckets
|
|
29
|
+
* pass through in the same convention instead of crashing or leaking kebab-case.
|
|
30
|
+
*/
|
|
31
|
+
export const FEED_BUCKETS = [
|
|
32
|
+
'analyzing',
|
|
33
|
+
'auto_fixing',
|
|
34
|
+
'good_to_merge',
|
|
35
|
+
'failed_when_merging',
|
|
36
|
+
'issues',
|
|
37
|
+
'needs_assignment',
|
|
38
|
+
'needs_shepherding',
|
|
39
|
+
'needs_fixing',
|
|
40
|
+
'awaiting_review',
|
|
41
|
+
'review_requested',
|
|
42
|
+
'snoozed',
|
|
43
|
+
];
|
|
44
|
+
export function normalizeFeedBucket(bucket) {
|
|
45
|
+
return bucket == null ? null : bucket.replace(/-/g, '_');
|
|
46
|
+
}
|
package/dist/triage/runner.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export interface RunTriageOptions {
|
|
|
16
16
|
* Returns the first one found, or null if none available.
|
|
17
17
|
*/
|
|
18
18
|
export declare function detectAgenticCLI(): AgenticCLI | null;
|
|
19
|
+
/**
|
|
20
|
+
* Build the CLI command + args for the detected agentic tool.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildCommand(cli: AgenticCLI, prompt: string, maxTurns: number): {
|
|
23
|
+
command: string;
|
|
24
|
+
args: string[];
|
|
25
|
+
};
|
|
19
26
|
/**
|
|
20
27
|
* Run pre-PR triage checks using parallel sub-agent processes.
|
|
21
28
|
*
|
package/dist/triage/runner.js
CHANGED
|
@@ -86,7 +86,7 @@ export function detectAgenticCLI() {
|
|
|
86
86
|
/**
|
|
87
87
|
* Build the CLI command + args for the detected agentic tool.
|
|
88
88
|
*/
|
|
89
|
-
function buildCommand(cli, prompt, maxTurns) {
|
|
89
|
+
export function buildCommand(cli, prompt, maxTurns) {
|
|
90
90
|
switch (cli) {
|
|
91
91
|
case 'claude':
|
|
92
92
|
return {
|
|
@@ -101,7 +101,7 @@ function buildCommand(cli, prompt, maxTurns) {
|
|
|
101
101
|
case 'codex':
|
|
102
102
|
return {
|
|
103
103
|
command: 'codex',
|
|
104
|
-
args: ['exec', prompt, '--full-auto'
|
|
104
|
+
args: ['exec', prompt, '--full-auto'],
|
|
105
105
|
};
|
|
106
106
|
case 'gemini':
|
|
107
107
|
return {
|
package/dist/types.d.ts
CHANGED
|
@@ -719,11 +719,52 @@ declare const TriageConfigSchema: z.ZodObject<{
|
|
|
719
719
|
maxTurns?: Partial<Record<"code-review" | "rules-validator" | "intent-drift", number>> | undefined;
|
|
720
720
|
timeoutMs?: number | undefined;
|
|
721
721
|
}>;
|
|
722
|
+
declare const PreferencesSchema: z.ZodObject<{
|
|
723
|
+
/** Run analysis automatically for PR webhook events (default: true) */
|
|
724
|
+
automatic_analysis: z.ZodOptional<z.ZodBoolean>;
|
|
725
|
+
/** Enable sandbox-backed verification for this repository */
|
|
726
|
+
sandbox_enabled: z.ZodOptional<z.ZodBoolean>;
|
|
727
|
+
/** Enroll submitted PRs in auto-merge by default */
|
|
728
|
+
auto_merge: z.ZodOptional<z.ZodBoolean>;
|
|
729
|
+
/** Allow Haystack to attempt automatic fixes */
|
|
730
|
+
auto_fix: z.ZodOptional<z.ZodBoolean>;
|
|
731
|
+
}, "strip", z.ZodTypeAny, {
|
|
732
|
+
automatic_analysis?: boolean | undefined;
|
|
733
|
+
sandbox_enabled?: boolean | undefined;
|
|
734
|
+
auto_merge?: boolean | undefined;
|
|
735
|
+
auto_fix?: boolean | undefined;
|
|
736
|
+
}, {
|
|
737
|
+
automatic_analysis?: boolean | undefined;
|
|
738
|
+
sandbox_enabled?: boolean | undefined;
|
|
739
|
+
auto_merge?: boolean | undefined;
|
|
740
|
+
auto_fix?: boolean | undefined;
|
|
741
|
+
}>;
|
|
722
742
|
export declare const HaystackConfigSchema: z.ZodObject<{
|
|
723
743
|
/** Config version (must be "1") */
|
|
724
744
|
version: z.ZodLiteral<"1">;
|
|
725
745
|
/** Project name (for display) */
|
|
726
746
|
name: z.ZodOptional<z.ZodString>;
|
|
747
|
+
/** Repository-level Haystack behavior */
|
|
748
|
+
preferences: z.ZodOptional<z.ZodObject<{
|
|
749
|
+
/** Run analysis automatically for PR webhook events (default: true) */
|
|
750
|
+
automatic_analysis: z.ZodOptional<z.ZodBoolean>;
|
|
751
|
+
/** Enable sandbox-backed verification for this repository */
|
|
752
|
+
sandbox_enabled: z.ZodOptional<z.ZodBoolean>;
|
|
753
|
+
/** Enroll submitted PRs in auto-merge by default */
|
|
754
|
+
auto_merge: z.ZodOptional<z.ZodBoolean>;
|
|
755
|
+
/** Allow Haystack to attempt automatic fixes */
|
|
756
|
+
auto_fix: z.ZodOptional<z.ZodBoolean>;
|
|
757
|
+
}, "strip", z.ZodTypeAny, {
|
|
758
|
+
automatic_analysis?: boolean | undefined;
|
|
759
|
+
sandbox_enabled?: boolean | undefined;
|
|
760
|
+
auto_merge?: boolean | undefined;
|
|
761
|
+
auto_fix?: boolean | undefined;
|
|
762
|
+
}, {
|
|
763
|
+
automatic_analysis?: boolean | undefined;
|
|
764
|
+
sandbox_enabled?: boolean | undefined;
|
|
765
|
+
auto_merge?: boolean | undefined;
|
|
766
|
+
auto_fix?: boolean | undefined;
|
|
767
|
+
}>>;
|
|
727
768
|
/** Package manager override (auto-detected if not specified) */
|
|
728
769
|
package_manager: z.ZodOptional<z.ZodEnum<["npm", "pnpm", "yarn", "bun"]>>;
|
|
729
770
|
/** Development server configuration */
|
|
@@ -1315,6 +1356,12 @@ export declare const HaystackConfigSchema: z.ZodObject<{
|
|
|
1315
1356
|
root?: string | undefined;
|
|
1316
1357
|
depends_on?: string[] | undefined;
|
|
1317
1358
|
}> | undefined;
|
|
1359
|
+
preferences?: {
|
|
1360
|
+
automatic_analysis?: boolean | undefined;
|
|
1361
|
+
sandbox_enabled?: boolean | undefined;
|
|
1362
|
+
auto_merge?: boolean | undefined;
|
|
1363
|
+
auto_fix?: boolean | undefined;
|
|
1364
|
+
} | undefined;
|
|
1318
1365
|
package_manager?: "npm" | "pnpm" | "yarn" | "bun" | undefined;
|
|
1319
1366
|
dev_server?: {
|
|
1320
1367
|
command: string;
|
|
@@ -1442,6 +1489,12 @@ export declare const HaystackConfigSchema: z.ZodObject<{
|
|
|
1442
1489
|
root?: string | undefined;
|
|
1443
1490
|
depends_on?: string[] | undefined;
|
|
1444
1491
|
}> | undefined;
|
|
1492
|
+
preferences?: {
|
|
1493
|
+
automatic_analysis?: boolean | undefined;
|
|
1494
|
+
sandbox_enabled?: boolean | undefined;
|
|
1495
|
+
auto_merge?: boolean | undefined;
|
|
1496
|
+
auto_fix?: boolean | undefined;
|
|
1497
|
+
} | undefined;
|
|
1445
1498
|
package_manager?: "npm" | "pnpm" | "yarn" | "bun" | undefined;
|
|
1446
1499
|
dev_server?: {
|
|
1447
1500
|
command: string;
|
|
@@ -1559,6 +1612,7 @@ export type NetworkConfig = z.infer<typeof NetworkConfigSchema>;
|
|
|
1559
1612
|
export type SecretDeclaration = z.infer<typeof SecretDeclarationSchema>;
|
|
1560
1613
|
export type MergeQueueConfig = z.infer<typeof MergeQueueConfigSchema>;
|
|
1561
1614
|
export type TriageConfig = z.infer<typeof TriageConfigSchema>;
|
|
1615
|
+
export type Preferences = z.infer<typeof PreferencesSchema>;
|
|
1562
1616
|
/**
|
|
1563
1617
|
* Project detection results
|
|
1564
1618
|
*/
|
package/dist/types.js
CHANGED
|
@@ -280,6 +280,19 @@ const TriageConfigSchema = z.object({
|
|
|
280
280
|
timeoutMs: z.number().int().positive().optional(),
|
|
281
281
|
});
|
|
282
282
|
// =============================================================================
|
|
283
|
+
// REPOSITORY PREFERENCES
|
|
284
|
+
// =============================================================================
|
|
285
|
+
const PreferencesSchema = z.object({
|
|
286
|
+
/** Run analysis automatically for PR webhook events (default: true) */
|
|
287
|
+
automatic_analysis: z.boolean().optional(),
|
|
288
|
+
/** Enable sandbox-backed verification for this repository */
|
|
289
|
+
sandbox_enabled: z.boolean().optional(),
|
|
290
|
+
/** Enroll submitted PRs in auto-merge by default */
|
|
291
|
+
auto_merge: z.boolean().optional(),
|
|
292
|
+
/** Allow Haystack to attempt automatic fixes */
|
|
293
|
+
auto_fix: z.boolean().optional(),
|
|
294
|
+
});
|
|
295
|
+
// =============================================================================
|
|
283
296
|
// MAIN CONFIG
|
|
284
297
|
// =============================================================================
|
|
285
298
|
export const HaystackConfigSchema = z.object({
|
|
@@ -287,6 +300,8 @@ export const HaystackConfigSchema = z.object({
|
|
|
287
300
|
version: z.literal('1'),
|
|
288
301
|
/** Project name (for display) */
|
|
289
302
|
name: z.string().optional(),
|
|
303
|
+
/** Repository-level Haystack behavior */
|
|
304
|
+
preferences: PreferencesSchema.optional(),
|
|
290
305
|
/** Package manager override (auto-detected if not specified) */
|
|
291
306
|
package_manager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).optional(),
|
|
292
307
|
/** Development server configuration */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared machine-readable envelope for the mutating commands
|
|
3
|
+
* (dismiss, mark-reviewed, undismiss, request-review, review).
|
|
4
|
+
*
|
|
5
|
+
* One schema for all of them (`haystack schema action`): every payload
|
|
6
|
+
* carries `action`, `status`, and `ref`, so an agent can drive any state
|
|
7
|
+
* change with a single parser. Success payloads go to stdout as the only
|
|
8
|
+
* stdout content; human/progress output stays on stderr in --json mode.
|
|
9
|
+
*/
|
|
10
|
+
export type ActionName = 'dismiss' | 'mark_reviewed' | 'undismiss' | 'request_review' | 'trigger_review' | 'clear_pending';
|
|
11
|
+
export interface ActionPayload extends Record<string, unknown> {
|
|
12
|
+
action: ActionName;
|
|
13
|
+
status: 'ok';
|
|
14
|
+
ref: string;
|
|
15
|
+
/** Human-readable statement of what changed. */
|
|
16
|
+
detail: string;
|
|
17
|
+
}
|
|
18
|
+
/** Print the success envelope (JSON mode only — human output is the caller's). */
|
|
19
|
+
export declare function emitActionPayload(payload: ActionPayload): void;
|
|
20
|
+
/**
|
|
21
|
+
* Print a failure (JSON error envelope on stdout when --json, red text on
|
|
22
|
+
* stderr always) and exit 1.
|
|
23
|
+
*/
|
|
24
|
+
export declare function failAction(json: boolean | undefined, action: ActionName, ref: string | null, message: string): never;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared machine-readable envelope for the mutating commands
|
|
3
|
+
* (dismiss, mark-reviewed, undismiss, request-review, review).
|
|
4
|
+
*
|
|
5
|
+
* One schema for all of them (`haystack schema action`): every payload
|
|
6
|
+
* carries `action`, `status`, and `ref`, so an agent can drive any state
|
|
7
|
+
* change with a single parser. Success payloads go to stdout as the only
|
|
8
|
+
* stdout content; human/progress output stays on stderr in --json mode.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { withSchema } from '../schema.js';
|
|
12
|
+
/** Print the success envelope (JSON mode only — human output is the caller's). */
|
|
13
|
+
export function emitActionPayload(payload) {
|
|
14
|
+
console.log(JSON.stringify(withSchema('action', payload), null, 2));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Print a failure (JSON error envelope on stdout when --json, red text on
|
|
18
|
+
* stderr always) and exit 1.
|
|
19
|
+
*/
|
|
20
|
+
export function failAction(json, action, ref, message) {
|
|
21
|
+
if (json) {
|
|
22
|
+
process.stdout.write(JSON.stringify(withSchema('action', { action, status: 'error', ref, error: message }), null, 2) + '\n');
|
|
23
|
+
}
|
|
24
|
+
console.error(chalk.red(`\n${message}\n`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
@@ -172,5 +172,16 @@ export interface AutoFixManifest {
|
|
|
172
172
|
timestamp: string;
|
|
173
173
|
resultSha?: string;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Null means "artifact does not exist (yet)" — 404s and transient
|
|
177
|
+
* network/parse failures. Auth failures (401/403) THROW a classified
|
|
178
|
+
* HaystackApiError instead: collapsing them into null used to make an
|
|
179
|
+
* expired token read as "not available yet — try again in a moment",
|
|
180
|
+
* sending agents into a retry loop when the fix was `haystack login`.
|
|
181
|
+
*/
|
|
175
182
|
export declare function fetchAutoFixManifest(owner: string, repo: string, prNumber: number, token?: string): Promise<AutoFixManifest | null>;
|
|
183
|
+
/**
|
|
184
|
+
* Null means "synthesis does not exist (yet)". Auth failures (401/403) THROW
|
|
185
|
+
* a classified HaystackApiError — see fetchAutoFixManifest for why.
|
|
186
|
+
*/
|
|
176
187
|
export declare function fetchRatingSynthesis(owner: string, repo: string, prNumber: number, token?: string): Promise<RatingSynthesis | null>;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Analysis is triggered automatically by the GitHub App webhook when a PR
|
|
8
8
|
* is created or updated — the CLI does not need to trigger it directly.
|
|
9
9
|
*/
|
|
10
|
+
import { classifyHttpError } from './haystack-api.js';
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Constants
|
|
12
13
|
// ============================================================================
|
|
@@ -187,7 +188,9 @@ export async function checkAnalysisReady(owner, repo, prNumber, token) {
|
|
|
187
188
|
if (response.status === 404)
|
|
188
189
|
return { status: 'pending' };
|
|
189
190
|
if (!response.ok) {
|
|
190
|
-
|
|
191
|
+
// Classified, actionable message — a bare "HTTP 403" used to send agents
|
|
192
|
+
// into a retry loop when the fix was `haystack login`.
|
|
193
|
+
return { status: 'error', message: (await classifyHttpError(response, 'Analysis status check')).message };
|
|
191
194
|
}
|
|
192
195
|
let data;
|
|
193
196
|
try {
|
|
@@ -336,12 +339,22 @@ export async function fetchAnalysisResults(owner, repo, prNumber, token) {
|
|
|
336
339
|
needsHumanReview,
|
|
337
340
|
};
|
|
338
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Null means "artifact does not exist (yet)" — 404s and transient
|
|
344
|
+
* network/parse failures. Auth failures (401/403) THROW a classified
|
|
345
|
+
* HaystackApiError instead: collapsing them into null used to make an
|
|
346
|
+
* expired token read as "not available yet — try again in a moment",
|
|
347
|
+
* sending agents into a retry loop when the fix was `haystack login`.
|
|
348
|
+
*/
|
|
339
349
|
export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
|
|
340
350
|
const url = v3ResultUrl(owner, repo, prNumber, 'auto-fix-manifest.json');
|
|
341
351
|
try {
|
|
342
352
|
const response = await fetch(url, {
|
|
343
353
|
headers: apiHeaders(token),
|
|
344
354
|
});
|
|
355
|
+
if (response.status === 401 || response.status === 403) {
|
|
356
|
+
throw await classifyHttpError(response, 'Auto-fix manifest fetch');
|
|
357
|
+
}
|
|
345
358
|
if (!response.ok)
|
|
346
359
|
return null;
|
|
347
360
|
const data = await response.json();
|
|
@@ -355,10 +368,17 @@ export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
|
|
|
355
368
|
}
|
|
356
369
|
return data;
|
|
357
370
|
}
|
|
358
|
-
catch {
|
|
371
|
+
catch (err) {
|
|
372
|
+
if (err instanceof Error && err.name === 'HaystackApiError')
|
|
373
|
+
throw err;
|
|
374
|
+
// Transient network/parse failure — same "not available" semantics as 404.
|
|
359
375
|
return null;
|
|
360
376
|
}
|
|
361
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Null means "synthesis does not exist (yet)". Auth failures (401/403) THROW
|
|
380
|
+
* a classified HaystackApiError — see fetchAutoFixManifest for why.
|
|
381
|
+
*/
|
|
362
382
|
export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
|
|
363
383
|
// Route through the auth-worker (/api/analysis/{prId}/rating_synthesis.json)
|
|
364
384
|
// so hsk_live tokens are introspected and the scope gate applies — see
|
|
@@ -368,6 +388,9 @@ export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
|
|
|
368
388
|
const response = await fetch(url, {
|
|
369
389
|
headers: apiHeaders(token),
|
|
370
390
|
});
|
|
391
|
+
if (response.status === 401 || response.status === 403) {
|
|
392
|
+
throw await classifyHttpError(response, 'Rating synthesis fetch');
|
|
393
|
+
}
|
|
371
394
|
if (!response.ok)
|
|
372
395
|
return null;
|
|
373
396
|
const data = await response.json();
|
|
@@ -384,7 +407,10 @@ export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
|
|
|
384
407
|
}
|
|
385
408
|
return data;
|
|
386
409
|
}
|
|
387
|
-
catch {
|
|
410
|
+
catch (err) {
|
|
411
|
+
if (err instanceof Error && err.name === 'HaystackApiError')
|
|
412
|
+
throw err;
|
|
413
|
+
// Transient network/parse failure — same "not available" semantics as 404.
|
|
388
414
|
return null;
|
|
389
415
|
}
|
|
390
416
|
}
|
package/dist/utils/auth.js
CHANGED
|
@@ -399,6 +399,13 @@ export async function resolveAuthContext(options = {}) {
|
|
|
399
399
|
// — that intent should not be overridden by a stale OAuth cookie.
|
|
400
400
|
const headless = await readHeadlessToken();
|
|
401
401
|
if (headless) {
|
|
402
|
+
// The headless token wins, but dropping an explicit --account silently
|
|
403
|
+
// would leave the caller authenticated as someone they didn't ask for —
|
|
404
|
+
// say so on stderr.
|
|
405
|
+
if (options.preferredLogin) {
|
|
406
|
+
console.error(`--account ${options.preferredLogin} is ignored: a headless token (${headless.label ?? 'token file'}) takes precedence. ` +
|
|
407
|
+
'Unset HAYSTACK_CLI_TOKEN or remove the token file (`haystack logout --headless`) to use saved accounts.');
|
|
408
|
+
}
|
|
402
409
|
return {
|
|
403
410
|
login: headless.label ?? 'headless',
|
|
404
411
|
token: headless.token,
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -78,7 +78,10 @@ export declare class UnsupportedRemoteUrlError extends Error {
|
|
|
78
78
|
*/
|
|
79
79
|
export declare function parseRemoteUrl(remoteName?: string): GitRemoteInfo;
|
|
80
80
|
/**
|
|
81
|
-
* Get the last commit message (first line only)
|
|
81
|
+
* Get the last commit message (first line only).
|
|
82
|
+
*
|
|
83
|
+
* Throws instead of guessing: this feeds the default PR title, and the old
|
|
84
|
+
* silent fallback ('Update') shipped meaningless titles when git failed.
|
|
82
85
|
*/
|
|
83
86
|
export declare function getLastCommitMessage(): string;
|
|
84
87
|
/**
|
package/dist/utils/git.js
CHANGED
|
@@ -22,8 +22,10 @@ export function getCurrentBranch() {
|
|
|
22
22
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
23
23
|
}).trim();
|
|
24
24
|
}
|
|
25
|
-
catch {
|
|
26
|
-
|
|
25
|
+
catch (err) {
|
|
26
|
+
const stderr = err?.stderr?.toString().trim();
|
|
27
|
+
throw new Error(`Failed to get the current branch${stderr ? ` (${stderr})` : ''}. ` +
|
|
28
|
+
'Run from inside a git repository.');
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
@@ -296,19 +298,25 @@ export function parseRemoteUrl(remoteName = 'origin') {
|
|
|
296
298
|
}
|
|
297
299
|
catch (err) {
|
|
298
300
|
// Propagate the "unrecognized URL" case as-is; wrap everything else (the
|
|
299
|
-
// `git remote get-url` command itself failing)
|
|
301
|
+
// `git remote get-url` command itself failing) with the underlying git
|
|
302
|
+
// error preserved — "No such remote 'origin'" is the actual diagnosis.
|
|
300
303
|
// Classify by type, not message text (see pr-rules.yml PR007).
|
|
301
304
|
if (err instanceof UnsupportedRemoteUrlError) {
|
|
302
305
|
throw err;
|
|
303
306
|
}
|
|
304
|
-
|
|
307
|
+
const stderr = err?.stderr?.toString().trim();
|
|
308
|
+
const cause = stderr || (err instanceof Error ? err.message : String(err));
|
|
309
|
+
throw new Error(`Failed to get remote URL for '${remoteName}': ${cause}`);
|
|
305
310
|
}
|
|
306
311
|
}
|
|
307
312
|
// ============================================================================
|
|
308
313
|
// Commit operations
|
|
309
314
|
// ============================================================================
|
|
310
315
|
/**
|
|
311
|
-
* Get the last commit message (first line only)
|
|
316
|
+
* Get the last commit message (first line only).
|
|
317
|
+
*
|
|
318
|
+
* Throws instead of guessing: this feeds the default PR title, and the old
|
|
319
|
+
* silent fallback ('Update') shipped meaningless titles when git failed.
|
|
312
320
|
*/
|
|
313
321
|
export function getLastCommitMessage() {
|
|
314
322
|
try {
|
|
@@ -317,8 +325,10 @@ export function getLastCommitMessage() {
|
|
|
317
325
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
318
326
|
}).trim();
|
|
319
327
|
}
|
|
320
|
-
catch {
|
|
321
|
-
|
|
328
|
+
catch (err) {
|
|
329
|
+
const stderr = err?.stderr?.toString().trim();
|
|
330
|
+
throw new Error(`Could not read the last commit message${stderr ? ` (${stderr})` : ''}. ` +
|
|
331
|
+
'Commit your changes first, or pass an explicit --title.');
|
|
322
332
|
}
|
|
323
333
|
}
|
|
324
334
|
/**
|
|
@@ -12,6 +12,21 @@ export interface CliIdentity {
|
|
|
12
12
|
login: string;
|
|
13
13
|
id: number;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Typed error for non-OK Haystack API responses. Callers classify by
|
|
17
|
+
* `status` (never by message substring) and the message always carries a
|
|
18
|
+
* remediation an agent can act on.
|
|
19
|
+
*/
|
|
20
|
+
export declare class HaystackApiError extends Error {
|
|
21
|
+
readonly status: number;
|
|
22
|
+
constructor(status: number, message: string);
|
|
23
|
+
get isAuthError(): boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build an actionable error for a non-OK response: what failed, the HTTP
|
|
27
|
+
* class it failed with, and the next command to run.
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyHttpError(response: Response, context: string): Promise<HaystackApiError>;
|
|
15
30
|
export declare function haystackJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
|
|
16
31
|
export declare function fetchCliIdentity(token: string): Promise<CliIdentity>;
|
|
17
32
|
export declare function fetchHaystackInstallations(token: string): Promise<UserInstallation[]>;
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
const HAYSTACK_API_BASE = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
|
|
2
|
+
/**
|
|
3
|
+
* Typed error for non-OK Haystack API responses. Callers classify by
|
|
4
|
+
* `status` (never by message substring) and the message always carries a
|
|
5
|
+
* remediation an agent can act on.
|
|
6
|
+
*/
|
|
7
|
+
export class HaystackApiError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
constructor(status, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'HaystackApiError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
get isAuthError() {
|
|
15
|
+
return this.status === 401 || this.status === 403;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
function headers(token, extra) {
|
|
3
19
|
const result = new Headers(extra);
|
|
4
20
|
result.set('Authorization', `Bearer ${token}`);
|
|
@@ -6,17 +22,52 @@ function headers(token, extra) {
|
|
|
6
22
|
result.set('User-Agent', 'Haystack-CLI');
|
|
7
23
|
return result;
|
|
8
24
|
}
|
|
9
|
-
|
|
10
|
-
|
|
25
|
+
/** Extract the server's own message if the body is JSON; truncate raw bodies. */
|
|
26
|
+
async function serverMessage(response) {
|
|
27
|
+
let text;
|
|
28
|
+
try {
|
|
29
|
+
text = await response.text();
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
// A transport failure while reading the error body is itself signal —
|
|
33
|
+
// say so instead of reporting it as "no detail" (the status-based
|
|
34
|
+
// classification in classifyHttpError stands either way).
|
|
35
|
+
return `error body unreadable: ${err instanceof Error ? err.message : String(err)}`;
|
|
36
|
+
}
|
|
11
37
|
if (!text)
|
|
12
|
-
return
|
|
38
|
+
return null;
|
|
13
39
|
try {
|
|
14
40
|
const parsed = JSON.parse(text);
|
|
15
|
-
return parsed.message ?? parsed.error ??
|
|
41
|
+
return parsed.message ?? parsed.error ?? null;
|
|
16
42
|
}
|
|
17
43
|
catch {
|
|
18
|
-
|
|
44
|
+
// Not JSON (HTML error page, plain text) — never dump a full raw body.
|
|
45
|
+
return text.slice(0, 200);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build an actionable error for a non-OK response: what failed, the HTTP
|
|
50
|
+
* class it failed with, and the next command to run.
|
|
51
|
+
*/
|
|
52
|
+
export async function classifyHttpError(response, context) {
|
|
53
|
+
const detail = await serverMessage(response);
|
|
54
|
+
const suffix = detail ? ` (${detail})` : '';
|
|
55
|
+
if (response.status === 401) {
|
|
56
|
+
return new HaystackApiError(401, `${context}: authentication failed${suffix}. Run \`haystack login\` to re-authenticate.`);
|
|
57
|
+
}
|
|
58
|
+
if (response.status === 403) {
|
|
59
|
+
return new HaystackApiError(403, `${context}: access denied${suffix}. Your token may lack access to this repository — run \`haystack auth list\` to check accounts, or \`haystack login\` to add one.`);
|
|
60
|
+
}
|
|
61
|
+
if (response.status === 404) {
|
|
62
|
+
return new HaystackApiError(404, `${context}: not found${suffix}. Check the PR number and repository.`);
|
|
63
|
+
}
|
|
64
|
+
if (response.status === 429) {
|
|
65
|
+
return new HaystackApiError(429, `${context}: rate limited${suffix}. Wait before retrying.`);
|
|
66
|
+
}
|
|
67
|
+
if (response.status >= 500) {
|
|
68
|
+
return new HaystackApiError(response.status, `${context}: Haystack server error (HTTP ${response.status})${suffix}. Retry in a moment.`);
|
|
19
69
|
}
|
|
70
|
+
return new HaystackApiError(response.status, `${context}: unexpected response (HTTP ${response.status})${suffix}.`);
|
|
20
71
|
}
|
|
21
72
|
export async function haystackJson(path, token, init = {}) {
|
|
22
73
|
const response = await fetch(`${HAYSTACK_API_BASE}${path}`, {
|
|
@@ -24,7 +75,7 @@ export async function haystackJson(path, token, init = {}) {
|
|
|
24
75
|
headers: headers(token, init.headers),
|
|
25
76
|
});
|
|
26
77
|
if (!response.ok) {
|
|
27
|
-
throw
|
|
78
|
+
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
28
79
|
}
|
|
29
80
|
return response.json();
|
|
30
81
|
}
|
|
@@ -46,7 +97,7 @@ export async function fetchAnalysisArtifact(owner, repo, prNumber, file, token)
|
|
|
46
97
|
if (response.status === 404)
|
|
47
98
|
return null;
|
|
48
99
|
if (!response.ok) {
|
|
49
|
-
throw
|
|
100
|
+
throw await classifyHttpError(response, `Analysis artifact ${file}`);
|
|
50
101
|
}
|
|
51
102
|
const data = await response.json();
|
|
52
103
|
if (!data || typeof data !== 'object' || !('download_url' in data) || !data.download_url) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared PR-identifier parsing for every command that takes a PR ref.
|
|
3
|
+
*
|
|
4
|
+
* One grammar, one error message, one repo-inference path. This used to be
|
|
5
|
+
* duplicated per command, and the copies drifted: different "accepted formats"
|
|
6
|
+
* lists (none mentioned the `#123` form the regex accepted), and one copy let
|
|
7
|
+
* a raw git error escape where the others gave a friendly hint. Commands must
|
|
8
|
+
* import from here rather than reimplementing the grammar.
|
|
9
|
+
*
|
|
10
|
+
* Accepted formats, in the order they are tried:
|
|
11
|
+
* https://github.com/owner/repo/pull/123 GitHub URL
|
|
12
|
+
* owner/repo#123 Fully qualified
|
|
13
|
+
* 123 or #123 PR number (repo inferred from the `origin` remote)
|
|
14
|
+
*/
|
|
15
|
+
export interface ParsedPR {
|
|
16
|
+
owner: string;
|
|
17
|
+
repo: string;
|
|
18
|
+
prNumber: number;
|
|
19
|
+
}
|
|
20
|
+
/** The accepted-formats block used in parse errors and help text. */
|
|
21
|
+
export declare function acceptedPrRefFormats(commandName: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Parse a PR identifier. `commandName` is interpolated into error messages so
|
|
24
|
+
* the agent can copy a corrected invocation verbatim (e.g. "triage",
|
|
25
|
+
* "pr get", "traces list").
|
|
26
|
+
*/
|
|
27
|
+
export declare function parsePrRef(identifier: string, commandName: string): ParsedPR;
|