@haystackeditor/cli 0.15.19 → 0.15.20
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 +86 -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/index.js +88 -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/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 +1 -1
- 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/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { dirname, join } from 'node:path';
|
|
|
25
25
|
import chalk from 'chalk';
|
|
26
26
|
import { Command, Option } from 'commander';
|
|
27
27
|
import { statusCommand } from './commands/status.js';
|
|
28
|
+
import { withSchema } from './schema.js';
|
|
28
29
|
import { initCommand } from './commands/init.js';
|
|
29
30
|
import { authListCommand, authUseCommand, loginCommand, logoutCommand, whoamiCommand, } from './commands/login.js';
|
|
30
31
|
import { handleAgenticTool, handleAutoMerge, handleAutoFix, handleWaitForReviewers, isAutoMergeEnabled, isAutoFixEnabled } from './commands/config.js';
|
|
@@ -46,6 +47,7 @@ import { setupCommand } from './commands/setup.js';
|
|
|
46
47
|
import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
|
|
47
48
|
import { registerWebhook, listWebhooks, rotateWebhookSecret, setWebhookEnabled, listDeliveries, replayDelivery, } from './commands/webhooks.js';
|
|
48
49
|
import { editRulesCommand, validateRulesCommand } from './commands/rules.js';
|
|
50
|
+
import { systemMapValidateCommand } from './commands/system-map.js';
|
|
49
51
|
import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeTokenCommand, } from './commands/tokens.js';
|
|
50
52
|
// `mcp` is imported lazily inside its command action (below), NOT at the top level.
|
|
51
53
|
// It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
|
|
@@ -53,12 +55,19 @@ import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeT
|
|
|
53
55
|
// like `triage`/`submit`. (0.15.12 shipped `mcp.js` but published its package.json
|
|
54
56
|
// WITHOUT `@modelcontextprotocol/sdk`, so the eager import here hard-crashed every
|
|
55
57
|
// command at startup with ERR_MODULE_NOT_FOUND.)
|
|
56
|
-
async function runPublicCommand(action) {
|
|
58
|
+
async function runPublicCommand(action, json) {
|
|
57
59
|
try {
|
|
58
60
|
await action();
|
|
59
61
|
}
|
|
60
62
|
catch (err) {
|
|
61
|
-
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
// In --json mode stdout must still carry one parseable document — emit the
|
|
65
|
+
// versioned error envelope (`haystack schema error`) instead of leaving
|
|
66
|
+
// stdout empty with prose on stderr.
|
|
67
|
+
if (json) {
|
|
68
|
+
process.stdout.write(JSON.stringify(withSchema('error', { status: 'error', error: message }), null, 2) + '\n');
|
|
69
|
+
}
|
|
70
|
+
console.error(chalk.red(message));
|
|
62
71
|
process.exitCode = 1;
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -112,7 +121,7 @@ program
|
|
|
112
121
|
.description('Onboarding wizard — scan repos for rules/policies and write Haystack config')
|
|
113
122
|
.option('--json', 'Agent mode: emit NDJSON events on stdout, read answers on stdin (no TTY prompts)')
|
|
114
123
|
.option('--repo <repo...>', 'Repo(s) to configure (owner/name); skips the repo picker')
|
|
115
|
-
.option('-y, --yes', 'Accept defaults non-interactively (keep all discovered items, confirm write)')
|
|
124
|
+
.option('-y, --yes', 'Accept defaults non-interactively (keep all discovered items, confirm write, ENABLE auto-merge — pass --no-auto-merge to opt out)')
|
|
116
125
|
// Defining --no-auto-merge also accepts --auto-merge; default is "ask" unless
|
|
117
126
|
// one is explicitly passed (detected via getOptionValueSource below).
|
|
118
127
|
.option('--no-auto-merge', 'Set auto-merge in the written config (--auto-merge / --no-auto-merge; omit to be asked)')
|
|
@@ -247,7 +256,9 @@ program
|
|
|
247
256
|
.option('--force', 'Skip pre-PR triage checks')
|
|
248
257
|
.addOption(new Option('--auto-fix', 'Alpha auto-fix for entitled repositories').hideHelp())
|
|
249
258
|
.option('--auto-merge', 'Apply auto-merge label (default: from .haystack.json, --no-auto-merge to disable)')
|
|
259
|
+
.option('--no-auto-merge', 'Do not apply the auto-merge label')
|
|
250
260
|
.option('--no-wait', 'Skip waiting for analysis results')
|
|
261
|
+
.option('--json', 'Machine-readable: one JSON document on stdout (see `haystack schema submit`), progress on stderr')
|
|
251
262
|
.option('--max-turns <n>', 'Max agentic turns per triage checker (overrides .haystack.json triage.maxTurns)', (v) => parseInt(v, 10))
|
|
252
263
|
.option('--triage-timeout <seconds>', 'Wall-clock timeout per triage checker in seconds (overrides .haystack.json triage.timeoutMs)', (v) => parseInt(v, 10))
|
|
253
264
|
.addHelpText('after', `
|
|
@@ -296,8 +307,22 @@ Review Routing:
|
|
|
296
307
|
needs-assignment queue (a teammate will pick it up)
|
|
297
308
|
• --review <username> Same label, plus requests review from that GitHub user
|
|
298
309
|
|
|
310
|
+
Machine-readable output (--json):
|
|
311
|
+
stdout carries exactly one JSON document (schema: \`haystack schema submit\`);
|
|
312
|
+
all progress goes to stderr. On success the payload includes the PR ref,
|
|
313
|
+
the resolved title/body and their sources, auto-merge/auto-fix state with
|
|
314
|
+
source, and the analysis outcome. On failure it is an error envelope
|
|
315
|
+
{ status: "error", error: "..." }.
|
|
316
|
+
|
|
317
|
+
Exit codes:
|
|
318
|
+
0 PR created/updated (including verdict "needs_input" — the PR exists;
|
|
319
|
+
read the verdict from the JSON or from \`haystack triage\`)
|
|
320
|
+
1 Hard failure (bad flags, auth, push, PR creation, blocking triage
|
|
321
|
+
findings, or the analysis status check itself failing)
|
|
322
|
+
|
|
299
323
|
Examples:
|
|
300
324
|
haystack submit # Recommended: triage, create PR, auto-merge queue
|
|
325
|
+
haystack submit --json # Agent mode: JSON on stdout, progress on stderr
|
|
301
326
|
haystack submit --force # Skip triage
|
|
302
327
|
haystack submit --no-wait # Don't wait for analysis
|
|
303
328
|
haystack submit --title "Fix auth" # Custom PR title
|
|
@@ -312,17 +337,23 @@ Examples:
|
|
|
312
337
|
haystack submit --max-turns 12 # Raise triage turn cap to 12 per checker
|
|
313
338
|
haystack submit --triage-timeout 300 # Raise triage wall-clock to 5 minutes
|
|
314
339
|
`)
|
|
315
|
-
.action(async (options) => {
|
|
316
|
-
// Resolve --auto-merge
|
|
317
|
-
|
|
340
|
+
.action(async (options, command) => {
|
|
341
|
+
// Resolve --auto-merge / --auto-fix defaults from .haystack.json when not
|
|
342
|
+
// explicitly set, and record WHERE each value came from so submit can
|
|
343
|
+
// announce the source (silent config-driven defaults are invisible to
|
|
344
|
+
// the caller otherwise).
|
|
345
|
+
const autoMergeFromCli = command.getOptionValueSource('autoMerge') === 'cli';
|
|
346
|
+
const autoFixFromCli = command.getOptionValueSource('autoFix') === 'cli';
|
|
347
|
+
if (!autoMergeFromCli) {
|
|
318
348
|
options.autoMerge = await isAutoMergeEnabled();
|
|
319
349
|
}
|
|
320
|
-
// Resolve --auto-fix default from .haystack.json (preferences.auto_fix).
|
|
321
350
|
// Auto-fix remains alpha — the explicit --auto-fix flag still surfaces
|
|
322
351
|
// discouragement warnings; opting in via repo config is the supported path.
|
|
323
|
-
if (
|
|
352
|
+
if (!autoFixFromCli) {
|
|
324
353
|
options.autoFix = await isAutoFixEnabled();
|
|
325
354
|
}
|
|
355
|
+
options.autoMergeSource = autoMergeFromCli ? 'flag' : 'config';
|
|
356
|
+
options.autoFixSource = autoFixFromCli ? 'flag' : 'config';
|
|
326
357
|
return submitCommand(options);
|
|
327
358
|
});
|
|
328
359
|
program
|
|
@@ -364,35 +395,36 @@ inbox
|
|
|
364
395
|
.command('list')
|
|
365
396
|
.description('List PRs in your Haystack inbox')
|
|
366
397
|
.option('--json', 'Output as JSON')
|
|
367
|
-
.action((options) => runPublicCommand(() => inboxListCommand(options)));
|
|
398
|
+
.action((options) => runPublicCommand(() => inboxListCommand(options), options.json));
|
|
368
399
|
const prProgram = program.command('pr').description('Inspect one pull request');
|
|
369
400
|
prProgram
|
|
370
401
|
.command('get <ref>')
|
|
371
402
|
.description('Get triage, merge blockers, and trace availability')
|
|
372
403
|
.option('--json', 'Output as JSON')
|
|
373
|
-
.action((ref, options) => runPublicCommand(() => prReadCommand(ref, options)));
|
|
404
|
+
.action((ref, options) => runPublicCommand(() => prReadCommand(ref, options), options.json));
|
|
374
405
|
program
|
|
375
406
|
.command('ask <ref> <question>')
|
|
376
407
|
.description('Ask Haystack about a pull request')
|
|
377
408
|
.option('--json', 'Output the answer and every consulted customer-facing source as JSON')
|
|
378
409
|
.option('--session <id>', 'Continue a previous Ask Haystack session')
|
|
379
|
-
.action((ref, question, options) => runPublicCommand(() => askHaystackCommand(ref, question, options)));
|
|
410
|
+
.action((ref, question, options) => runPublicCommand(() => askHaystackCommand(ref, question, options), options.json));
|
|
380
411
|
const traces = program.command('traces').description('Inspect customer-owned Entire traces');
|
|
381
412
|
traces
|
|
382
413
|
.command('list <ref>')
|
|
383
414
|
.description('List retained checkpoints for a pull request')
|
|
384
415
|
.option('--json', 'Output as JSON')
|
|
385
|
-
.action((ref, options) => runPublicCommand(() => tracesListCommand(ref, options)));
|
|
416
|
+
.action((ref, options) => runPublicCommand(() => tracesListCommand(ref, options), options.json));
|
|
386
417
|
traces
|
|
387
418
|
.command('get <ref> <checkpoint>')
|
|
388
419
|
.description('Get retained transcript chunks for one checkpoint')
|
|
389
420
|
.option('--cursor <cursor>', 'Zero-based chunk cursor')
|
|
390
421
|
.option('--limit <count>', 'Chunks per page', '20')
|
|
391
422
|
.option('--json', 'Output as JSON')
|
|
392
|
-
.action((ref, checkpoint, options) => runPublicCommand(() => tracesGetCommand(ref, checkpoint, options)));
|
|
423
|
+
.action((ref, checkpoint, options) => runPublicCommand(() => tracesGetCommand(ref, checkpoint, options), options.json));
|
|
393
424
|
program
|
|
394
425
|
.command('dismiss <pr>')
|
|
395
426
|
.description('Dismiss analysis findings for a PR')
|
|
427
|
+
.option('--json', 'Machine-readable output (see `haystack schema action`)')
|
|
396
428
|
.addHelpText('after', `
|
|
397
429
|
Dismiss analysis findings for a PR, moving it from "Issues Found" to
|
|
398
430
|
"Good to Merge" in the Haystack feed.
|
|
@@ -414,6 +446,7 @@ Examples:
|
|
|
414
446
|
program
|
|
415
447
|
.command('mark-reviewed <pr>')
|
|
416
448
|
.description('Mark human review as not needed for a PR')
|
|
449
|
+
.option('--json', 'Machine-readable output (see `haystack schema action`)')
|
|
417
450
|
.addHelpText('after', `
|
|
418
451
|
Mark human review as not needed for a PR, moving it from "Needs Review"
|
|
419
452
|
to "Good to Merge" in the Haystack feed.
|
|
@@ -435,6 +468,7 @@ Examples:
|
|
|
435
468
|
program
|
|
436
469
|
.command('undismiss <pr>')
|
|
437
470
|
.description('Undo a dismiss or mark-reviewed override for a PR')
|
|
471
|
+
.option('--json', 'Machine-readable output (see `haystack schema action`)')
|
|
438
472
|
.addHelpText('after', `
|
|
439
473
|
Clear all overrides (dismissed findings and/or review-not-needed) for a PR,
|
|
440
474
|
returning it to its original feed bucket.
|
|
@@ -452,8 +486,9 @@ Examples:
|
|
|
452
486
|
.action(undismissCommand);
|
|
453
487
|
program
|
|
454
488
|
.command('review [pr]')
|
|
455
|
-
.description('Trigger a fresh Haystack analysis for a PR')
|
|
489
|
+
.description('Trigger a fresh Haystack analysis for a PR (machine analysis — for HUMAN review use request-review)')
|
|
456
490
|
.option('--no-wait', 'Exit after triggering instead of waiting for results')
|
|
491
|
+
.option('--json', 'Machine-readable output (see `haystack schema action`)')
|
|
457
492
|
.addHelpText('after', `
|
|
458
493
|
Re-run full Haystack analysis on a PR's current head.
|
|
459
494
|
|
|
@@ -481,6 +516,7 @@ Examples:
|
|
|
481
516
|
program
|
|
482
517
|
.command('request-review <pr> [reviewer]')
|
|
483
518
|
.description('Tag a PR as needing human review')
|
|
519
|
+
.option('--json', 'Machine-readable output (see `haystack schema action`)')
|
|
484
520
|
.addHelpText('after', `
|
|
485
521
|
Tag any PR with "needs human review", even after it was created.
|
|
486
522
|
Adds the haystack:needs-review label to the PR, moving it from
|
|
@@ -551,9 +587,9 @@ config
|
|
|
551
587
|
.description('Auto-merge safe PRs submitted via haystack submit (on|off|status)')
|
|
552
588
|
.addHelpText('after', `
|
|
553
589
|
Actions:
|
|
554
|
-
on, enable
|
|
555
|
-
off, disable Disable auto-merge
|
|
556
|
-
status
|
|
590
|
+
on, enable, true Enable auto-merge for safe PRs
|
|
591
|
+
off, disable, false Disable auto-merge
|
|
592
|
+
status Show current status (default)
|
|
557
593
|
|
|
558
594
|
When enabled, PRs submitted via \`haystack submit\` will be
|
|
559
595
|
automatically merged if Haystack analysis finds no issues
|
|
@@ -571,9 +607,9 @@ config
|
|
|
571
607
|
.description('(Alpha) Auto-fix mechanical issues on PRs submitted via haystack submit (on|off|status)')
|
|
572
608
|
.addHelpText('after', `
|
|
573
609
|
Actions:
|
|
574
|
-
on, enable
|
|
575
|
-
off, disable Disable auto-fix
|
|
576
|
-
status
|
|
610
|
+
on, enable, true Enable auto-fix for mechanical issues
|
|
611
|
+
off, disable, false Disable auto-fix
|
|
612
|
+
status Show current status (default)
|
|
577
613
|
|
|
578
614
|
⚠ Auto-fix is alpha. When enabled, PRs submitted via \`haystack submit\`
|
|
579
615
|
are labeled haystack:auto-fix. The Haystack analysis pipeline then
|
|
@@ -882,6 +918,38 @@ rules
|
|
|
882
918
|
process.exit(1);
|
|
883
919
|
}
|
|
884
920
|
});
|
|
921
|
+
// ─── system-map ──────────────────────────────────────────────────────────────
|
|
922
|
+
const systemMap = program
|
|
923
|
+
.command('system-map')
|
|
924
|
+
.description('Manage .haystack/system.yml — runtime facts that help Haystack QA run your system');
|
|
925
|
+
systemMap
|
|
926
|
+
.command('validate')
|
|
927
|
+
.description('Check .haystack/system.yml against the rules Haystack QA will apply')
|
|
928
|
+
.option('--json', 'Machine-readable result')
|
|
929
|
+
.addHelpText('after', `
|
|
930
|
+
The system map holds VERIFIED facts about how your system works — install/
|
|
931
|
+
build commands, how services boot and signal readiness, seed/reset commands,
|
|
932
|
+
dev-only test logins, external services that must never be touched live, and
|
|
933
|
+
prebuilt CI artifacts. Haystack QA uses these facts to run more of what it
|
|
934
|
+
already wants to test; the file never selects tests.
|
|
935
|
+
|
|
936
|
+
Author it with the /map-your-system skill (haystack skills install), then run
|
|
937
|
+
this before committing. Checks: valid YAML + version, no test-selection keys,
|
|
938
|
+
no secret values, non-empty boot/login/run commands, artifact declarations.
|
|
939
|
+
|
|
940
|
+
Examples:
|
|
941
|
+
haystack system-map validate
|
|
942
|
+
haystack system-map validate --json
|
|
943
|
+
`)
|
|
944
|
+
.action(async (opts) => {
|
|
945
|
+
try {
|
|
946
|
+
await systemMapValidateCommand(opts);
|
|
947
|
+
}
|
|
948
|
+
catch (err) {
|
|
949
|
+
console.error(chalk.red('validate failed:'), err instanceof Error ? err.message : err);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
885
953
|
// ─── mcp ─────────────────────────────────────────────────────────────────────
|
|
886
954
|
program
|
|
887
955
|
.command('mcp')
|
package/dist/schema.d.ts
CHANGED
|
@@ -13,9 +13,13 @@ export declare const SCHEMA_VERSIONS: {
|
|
|
13
13
|
readonly triage: "2.0.0";
|
|
14
14
|
readonly setup: "1.0.0";
|
|
15
15
|
readonly pr: "2.0.0";
|
|
16
|
+
readonly 'pr-status': "1.0.0";
|
|
16
17
|
readonly inbox: "1.0.0";
|
|
17
18
|
readonly ask: "1.0.0";
|
|
18
19
|
readonly traces: "1.0.0";
|
|
20
|
+
readonly submit: "1.0.0";
|
|
21
|
+
readonly action: "1.0.0";
|
|
22
|
+
readonly error: "1.0.0";
|
|
19
23
|
};
|
|
20
24
|
export type SchemaName = keyof typeof SCHEMA_VERSIONS;
|
|
21
25
|
/** Wrap a payload with the `schema_version` envelope. */
|
package/dist/schema.js
CHANGED
|
@@ -13,9 +13,13 @@ export const SCHEMA_VERSIONS = {
|
|
|
13
13
|
triage: '2.0.0',
|
|
14
14
|
setup: '1.0.0',
|
|
15
15
|
pr: '2.0.0',
|
|
16
|
+
'pr-status': '1.0.0',
|
|
16
17
|
inbox: '1.0.0',
|
|
17
18
|
ask: '1.0.0',
|
|
18
19
|
traces: '1.0.0',
|
|
20
|
+
submit: '1.0.0',
|
|
21
|
+
action: '1.0.0',
|
|
22
|
+
error: '1.0.0',
|
|
19
23
|
};
|
|
20
24
|
/** Wrap a payload with the `schema_version` envelope. */
|
|
21
25
|
export function withSchema(name, payload) {
|
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 {
|
|
@@ -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[]>;
|