@haystackeditor/cli 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/commands/config.js +81 -66
- package/dist/commands/dismiss.js +2 -1
- package/dist/commands/request-review.d.ts +15 -0
- package/dist/commands/request-review.js +95 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +274 -2
- package/dist/commands/submit.js +1 -1
- package/dist/commands/triage.js +115 -23
- package/dist/index.d.ts +1 -0
- package/dist/index.js +43 -18
- package/dist/triage/prompts.js +13 -5
- package/dist/triage/runner.js +1 -1
- package/dist/triage/types.d.ts +1 -1
- package/dist/types.d.ts +132 -26
- package/dist/types.js +42 -5
- package/dist/utils/analysis-api.d.ts +16 -0
- package/dist/utils/analysis-api.js +23 -5
- package/dist/utils/telemetry.d.ts +17 -0
- package/dist/utils/telemetry.js +109 -0
- package/package.json +1 -1
- package/dist/commands/secrets.d.ts +0 -33
- package/dist/commands/secrets.js +0 -216
- package/dist/tools/fixtures.d.ts +0 -38
- package/dist/tools/fixtures.js +0 -199
- package/dist/tools/setup.d.ts +0 -43
- package/dist/tools/setup.js +0 -597
- package/dist/utils/skill.d.ts +0 -10
- package/dist/utils/skill.js +0 -1671
package/dist/commands/setup.js
CHANGED
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* 5. Scan for review policies
|
|
10
10
|
* 6. Review discovered items (toggle on/off)
|
|
11
11
|
* 7. Confirm & write .haystack.json to selected repos
|
|
12
|
+
* 8. Install session tracking (Entire CLI + .entire/settings.json + local hooks)
|
|
12
13
|
*/
|
|
13
14
|
import chalk from 'chalk';
|
|
14
15
|
import inquirer from 'inquirer';
|
|
16
|
+
import { execFileSync, execSync } from 'child_process';
|
|
17
|
+
import { mkdirSync } from 'fs';
|
|
18
|
+
import { basename, join } from 'path';
|
|
15
19
|
import { loadToken } from './login.js';
|
|
20
|
+
import { hooksInstall } from './hooks.js';
|
|
21
|
+
import { findGitRoot } from '../utils/hooks.js';
|
|
22
|
+
import { trackError, trackSetupEvent } from '../utils/telemetry.js';
|
|
16
23
|
// =============================================================================
|
|
17
24
|
// Constants
|
|
18
25
|
// =============================================================================
|
|
@@ -433,7 +440,7 @@ async function stepConfirm(selectedRepos, rules, signals, policies, token) {
|
|
|
433
440
|
]);
|
|
434
441
|
if (!confirmed) {
|
|
435
442
|
console.log(chalk.yellow('\n Setup cancelled.\n'));
|
|
436
|
-
return;
|
|
443
|
+
return false;
|
|
437
444
|
}
|
|
438
445
|
// Write to each repo
|
|
439
446
|
const failedRepos = [];
|
|
@@ -458,6 +465,7 @@ async function stepConfirm(selectedRepos, rules, signals, policies, token) {
|
|
|
458
465
|
process.exit(1);
|
|
459
466
|
}
|
|
460
467
|
console.log(chalk.green('\n Setup complete!\n'));
|
|
468
|
+
return true;
|
|
461
469
|
}
|
|
462
470
|
// =============================================================================
|
|
463
471
|
// Main command
|
|
@@ -492,5 +500,269 @@ export async function setupCommand() {
|
|
|
492
500
|
// Step 5: Review
|
|
493
501
|
const reviewed = await stepReview(rules, signals, policies);
|
|
494
502
|
// Step 6: Confirm & write
|
|
495
|
-
await stepConfirm(selectedRepos, reviewed.rules, reviewed.signals, reviewed.policies, token);
|
|
503
|
+
const confirmed = await stepConfirm(selectedRepos, reviewed.rules, reviewed.signals, reviewed.policies, token);
|
|
504
|
+
if (!confirmed)
|
|
505
|
+
return;
|
|
506
|
+
// Step 7: Install Entire CLI (session tracking)
|
|
507
|
+
await stepInstallEntire(selectedRepos, token);
|
|
508
|
+
}
|
|
509
|
+
// =============================================================================
|
|
510
|
+
// Step 7: Install Entire CLI for session tracking
|
|
511
|
+
// =============================================================================
|
|
512
|
+
const ENTIRE_VERSION = 'v0.5.3-haystack.1';
|
|
513
|
+
const ENTIRE_RELEASE_BASE = `https://github.com/haystackeditor/cli/releases/download/${ENTIRE_VERSION}`;
|
|
514
|
+
function getEntireBinaryName() {
|
|
515
|
+
const platform = process.platform === 'darwin' ? 'darwin'
|
|
516
|
+
: process.platform === 'linux' ? 'linux'
|
|
517
|
+
: null;
|
|
518
|
+
if (!platform)
|
|
519
|
+
return null; // unsupported (Windows, FreeBSD, etc.)
|
|
520
|
+
const arch = process.arch === 'arm64' ? 'arm64'
|
|
521
|
+
: process.arch === 'x64' ? 'amd64'
|
|
522
|
+
: null;
|
|
523
|
+
if (!arch)
|
|
524
|
+
return null; // unsupported (s390x, ppc64, etc.)
|
|
525
|
+
return `entire_${platform}_${arch}`;
|
|
526
|
+
}
|
|
527
|
+
function isEntireInstalled() {
|
|
528
|
+
// Check the canonical install location first, then fall back to PATH
|
|
529
|
+
const haystackBin = process.env.HOME
|
|
530
|
+
? join(process.env.HOME, '.haystack', 'bin', 'entire')
|
|
531
|
+
: null;
|
|
532
|
+
const candidates = haystackBin ? [haystackBin, 'entire'] : ['entire'];
|
|
533
|
+
for (const bin of candidates) {
|
|
534
|
+
try {
|
|
535
|
+
const version = execSync(`"${bin}" --version 2>&1`, { encoding: 'utf-8' }).trim();
|
|
536
|
+
return {
|
|
537
|
+
installed: true,
|
|
538
|
+
isHaystackFork: version.includes('haystack'),
|
|
539
|
+
version,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// Try next candidate
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return { installed: false, isHaystackFork: false, version: '' };
|
|
547
|
+
}
|
|
548
|
+
async function installEntireBinary() {
|
|
549
|
+
const binaryName = getEntireBinaryName();
|
|
550
|
+
if (!binaryName) {
|
|
551
|
+
console.log(chalk.yellow(` Entire CLI is not available for ${process.platform}/${process.arch}.`));
|
|
552
|
+
console.log(chalk.dim(' Supported: macOS (Intel/Apple Silicon), Linux (x86_64/ARM64)'));
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
const url = `${ENTIRE_RELEASE_BASE}/${binaryName}`;
|
|
556
|
+
const installDir = process.env.HOME
|
|
557
|
+
? join(process.env.HOME, '.haystack', 'bin')
|
|
558
|
+
: '/usr/local/bin';
|
|
559
|
+
try {
|
|
560
|
+
mkdirSync(installDir, { recursive: true });
|
|
561
|
+
const targetPath = join(installDir, 'entire');
|
|
562
|
+
console.log(chalk.dim(` Downloading ${ENTIRE_VERSION}...`));
|
|
563
|
+
execFileSync('curl', ['-fsSL', url, '-o', targetPath], { stdio: 'pipe' });
|
|
564
|
+
execFileSync('chmod', ['+x', targetPath], { stdio: 'pipe' });
|
|
565
|
+
// Verify using absolute path (don't rely on PATH)
|
|
566
|
+
const version = execFileSync(targetPath, ['--version'], { encoding: 'utf-8' }).trim();
|
|
567
|
+
if (!version.includes('haystack')) {
|
|
568
|
+
console.log(chalk.red(` Unexpected version: ${version}`));
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
console.log(chalk.green(` ✓ Installed: ${version.split('\n')[0]}`));
|
|
572
|
+
// Check if install dir is on PATH
|
|
573
|
+
const pathDirs = (process.env.PATH || '').split(':');
|
|
574
|
+
if (!pathDirs.includes(installDir)) {
|
|
575
|
+
console.log(chalk.yellow(`\n Note: ${installDir} is not on your PATH.`));
|
|
576
|
+
console.log(chalk.dim(` Add to your shell profile: export PATH="${installDir}:$PATH"\n`));
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
582
|
+
console.log(chalk.red(` Failed to install Entire CLI: ${errorMsg}`));
|
|
583
|
+
console.log(chalk.dim(` Manual install: curl -fsSL ${url} -o ~/.haystack/bin/entire && chmod +x ~/.haystack/bin/entire`));
|
|
584
|
+
trackError('entire_install_failed', {
|
|
585
|
+
error_message: errorMsg,
|
|
586
|
+
error_stack: err instanceof Error ? err.stack : undefined,
|
|
587
|
+
entire_version: ENTIRE_VERSION,
|
|
588
|
+
install_dir: installDir,
|
|
589
|
+
});
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Write .entire/settings.json with skip_raw_transcript via the GitHub API.
|
|
595
|
+
* selectedRepos are GitHub full names (owner/repo), not local paths.
|
|
596
|
+
*/
|
|
597
|
+
async function configureEntireSettingsViaAPI(repoFullName, token) {
|
|
598
|
+
const [owner, repo] = repoFullName.split('/');
|
|
599
|
+
const filePath = '.entire/settings.json';
|
|
600
|
+
const headers = {
|
|
601
|
+
Authorization: `Bearer ${token}`,
|
|
602
|
+
Accept: 'application/vnd.github.v3+json',
|
|
603
|
+
'User-Agent': 'Haystack-CLI',
|
|
604
|
+
'Content-Type': 'application/json',
|
|
605
|
+
};
|
|
606
|
+
// Fetch existing .entire/settings.json (if any)
|
|
607
|
+
let existingSha = null;
|
|
608
|
+
let settings = { enabled: true };
|
|
609
|
+
const getResp = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/contents/${filePath}`, { headers });
|
|
610
|
+
if (getResp.ok) {
|
|
611
|
+
const data = (await getResp.json());
|
|
612
|
+
existingSha = data.sha;
|
|
613
|
+
try {
|
|
614
|
+
settings = JSON.parse(Buffer.from(data.content, 'base64').toString('utf-8'));
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// Malformed JSON — keep sha for update but use fresh defaults
|
|
618
|
+
console.log(chalk.dim(` Existing .entire/settings.json in ${repoFullName} is malformed — will repair`));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else if (getResp.status !== 404) {
|
|
622
|
+
// Non-404 failure (auth, network, rate limit) — don't guess, surface it
|
|
623
|
+
const errText = await getResp.text().catch(() => '');
|
|
624
|
+
const error = new Error(`Failed to read .entire/settings.json: ${getResp.status} ${errText}`);
|
|
625
|
+
trackError('entire_settings_read_failed', {
|
|
626
|
+
error_message: error.message,
|
|
627
|
+
repo: repoFullName,
|
|
628
|
+
status: getResp.status,
|
|
629
|
+
});
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
// 404 → file doesn't exist, existingSha stays null → PUT will create
|
|
633
|
+
// Add skip_raw_transcript
|
|
634
|
+
const strategyOptions = (settings.strategy_options || {});
|
|
635
|
+
strategyOptions.skip_raw_transcript = true;
|
|
636
|
+
settings.strategy_options = strategyOptions;
|
|
637
|
+
const content = Buffer.from(JSON.stringify(settings, null, 2) + '\n').toString('base64');
|
|
638
|
+
const body = {
|
|
639
|
+
message: 'chore: enable skip_raw_transcript for Entire CLI\n\nPrevents committing raw 50MB session transcripts to git.',
|
|
640
|
+
content,
|
|
641
|
+
};
|
|
642
|
+
if (existingSha)
|
|
643
|
+
body.sha = existingSha;
|
|
644
|
+
const putResp = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/contents/${filePath}`, {
|
|
645
|
+
method: 'PUT',
|
|
646
|
+
headers,
|
|
647
|
+
body: JSON.stringify(body),
|
|
648
|
+
});
|
|
649
|
+
if (!putResp.ok) {
|
|
650
|
+
const errText = await putResp.text().catch(() => '');
|
|
651
|
+
const error = new Error(`GitHub API ${putResp.status}: ${errText}`);
|
|
652
|
+
trackError('entire_settings_write_failed', {
|
|
653
|
+
error_message: error.message,
|
|
654
|
+
repo: repoFullName,
|
|
655
|
+
status: putResp.status,
|
|
656
|
+
had_existing: !!existingSha,
|
|
657
|
+
});
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function stepInstallEntire(selectedRepos, token) {
|
|
662
|
+
console.log(chalk.bold('\n Step 7: Session tracking (Entire CLI)\n'));
|
|
663
|
+
const status = isEntireInstalled();
|
|
664
|
+
let binaryReady = status.installed && status.isHaystackFork;
|
|
665
|
+
let userConsented = true; // true if user said yes or was already installed
|
|
666
|
+
if (binaryReady) {
|
|
667
|
+
console.log(chalk.dim(` Entire CLI already installed (${status.version.split('\n')[0]})`));
|
|
668
|
+
}
|
|
669
|
+
else if (status.installed) {
|
|
670
|
+
console.log(chalk.dim(` Stock Entire CLI detected: ${status.version.split('\n')[0]}`));
|
|
671
|
+
const { upgrade } = await inquirer.prompt([
|
|
672
|
+
{
|
|
673
|
+
type: 'confirm',
|
|
674
|
+
name: 'upgrade',
|
|
675
|
+
message: 'Upgrade to Haystack fork? (adds transcript optimization — saves GB of git storage)',
|
|
676
|
+
default: true,
|
|
677
|
+
},
|
|
678
|
+
]);
|
|
679
|
+
if (upgrade) {
|
|
680
|
+
binaryReady = await installEntireBinary();
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
userConsented = false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const { install } = await inquirer.prompt([
|
|
688
|
+
{
|
|
689
|
+
type: 'confirm',
|
|
690
|
+
name: 'install',
|
|
691
|
+
message: 'Install Entire CLI? (tracks AI coding sessions for analysis)',
|
|
692
|
+
default: true,
|
|
693
|
+
},
|
|
694
|
+
]);
|
|
695
|
+
if (install) {
|
|
696
|
+
binaryReady = await installEntireBinary();
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
userConsented = false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (!userConsented) {
|
|
703
|
+
console.log(chalk.dim(' Skipping session tracking.\n'));
|
|
704
|
+
}
|
|
705
|
+
// Configure .entire/settings.json on each repo via GitHub API
|
|
706
|
+
// (works even if binary install failed — settings are remote; skip if user declined)
|
|
707
|
+
if (userConsented) {
|
|
708
|
+
let configured = 0;
|
|
709
|
+
for (const repoFullName of selectedRepos) {
|
|
710
|
+
try {
|
|
711
|
+
process.stdout.write(chalk.dim(` Configuring ${repoFullName}...`));
|
|
712
|
+
await configureEntireSettingsViaAPI(repoFullName, token);
|
|
713
|
+
process.stdout.write('\r\x1b[K');
|
|
714
|
+
console.log(chalk.green(` ✓ ${repoFullName}`));
|
|
715
|
+
configured++;
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
process.stdout.write('\r\x1b[K');
|
|
719
|
+
console.log(chalk.yellow(` ⚠ ${repoFullName}: ${err instanceof Error ? err.message : err}`));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (configured > 0) {
|
|
723
|
+
console.log(chalk.green(`\n ✓ Session tracking configured on ${configured} repo(s) (raw transcripts excluded)\n`));
|
|
724
|
+
trackSetupEvent('entire_configured', { configured, total: selectedRepos.length });
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
console.log(chalk.yellow('\n ⚠ Could not configure session tracking on any repos.\n'));
|
|
728
|
+
trackError('entire_config_all_failed', { total: selectedRepos.length });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Offer to install local git hooks if cwd is inside one of the selected repos
|
|
732
|
+
const gitRoot = findGitRoot();
|
|
733
|
+
if (gitRoot) {
|
|
734
|
+
const gitDirName = basename(gitRoot).toLowerCase();
|
|
735
|
+
const repoName = selectedRepos.find((r) => {
|
|
736
|
+
const name = r.split('/')[1]?.replace(/\.git$/, '');
|
|
737
|
+
return name && gitDirName === name.toLowerCase();
|
|
738
|
+
});
|
|
739
|
+
if (repoName) {
|
|
740
|
+
try {
|
|
741
|
+
const { installHooks } = await inquirer.prompt([
|
|
742
|
+
{
|
|
743
|
+
type: 'confirm',
|
|
744
|
+
name: 'installHooks',
|
|
745
|
+
message: `Install git hooks locally for ${repoName}? (agent detection, truncation checking, LLM rules)`,
|
|
746
|
+
default: true,
|
|
747
|
+
},
|
|
748
|
+
]);
|
|
749
|
+
if (installHooks) {
|
|
750
|
+
console.log('');
|
|
751
|
+
await hooksInstall({ skipEntire: true, force: true });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
755
|
+
console.log(chalk.yellow(` ⚠ Hook installation failed: ${err instanceof Error ? err.message : err}`));
|
|
756
|
+
console.log(chalk.dim(' You can install hooks later with: haystack hooks install'));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
console.log(chalk.dim(' To install git hooks in a repo, cd into it and run:'));
|
|
761
|
+
console.log(chalk.dim(' haystack hooks install\n'));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
console.log(chalk.dim(' To install git hooks in a repo, cd into it and run:'));
|
|
766
|
+
console.log(chalk.dim(' haystack hooks install\n'));
|
|
767
|
+
}
|
|
496
768
|
}
|
package/dist/commands/submit.js
CHANGED
|
@@ -51,7 +51,7 @@ function checkerDisplayName(checker) {
|
|
|
51
51
|
switch (checker) {
|
|
52
52
|
case 'code-review': return 'Code Review';
|
|
53
53
|
case 'rules-validator': return 'Rules Validator';
|
|
54
|
-
case 'intent-drift': return '
|
|
54
|
+
case 'intent-drift': return 'Instruction Drift';
|
|
55
55
|
default: return checker;
|
|
56
56
|
}
|
|
57
57
|
}
|
package/dist/commands/triage.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* haystack triage https://github.com/owner/repo/pull/123
|
|
11
11
|
*/
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { checkAnalysisReady, fetchRatingSynthesis } from '../utils/analysis-api.js';
|
|
13
|
+
import { checkAnalysisReady, fetchRatingSynthesis, fetchAutoFixManifest } from '../utils/analysis-api.js';
|
|
14
14
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
15
15
|
import { loadToken } from './login.js';
|
|
16
16
|
// ============================================================================
|
|
@@ -83,7 +83,7 @@ function severityIcon(severity) {
|
|
|
83
83
|
default: return chalk.dim('-');
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
function printRichOutput(pr, synthesis) {
|
|
86
|
+
function printRichOutput(pr, synthesis, manifest) {
|
|
87
87
|
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
88
88
|
console.log(chalk.bold('\n' + '━'.repeat(60)));
|
|
89
89
|
console.log(chalk.bold(` Haystack Analysis: ${pr.owner}/${pr.repo}#${pr.prNumber}`));
|
|
@@ -117,22 +117,56 @@ function printRichOutput(pr, synthesis) {
|
|
|
117
117
|
console.log(` ${chalk.dim('Checks:')} ${parts.join(' ')}`);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
//
|
|
120
|
+
// Auto-fix manifest — show what the fixer did vs left for human review
|
|
121
|
+
if (manifest && (manifest.fixedItems.length > 0 || manifest.leftForReview.length > 0)) {
|
|
122
|
+
console.log(chalk.bold('\n Auto-Fixer Results\n'));
|
|
123
|
+
if (manifest.fixedItems.length > 0) {
|
|
124
|
+
console.log(` ${chalk.green.bold(`Fixed (${manifest.fixedItems.length})`)}`);
|
|
125
|
+
for (const item of manifest.fixedItems) {
|
|
126
|
+
console.log(` ${chalk.green(' ✓')} ${chalk.dim(`[${item.category}]`)} ${item.summary}`);
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
if (manifest.leftForReview.length > 0) {
|
|
131
|
+
console.log(` ${chalk.yellow.bold(`Left for you (${manifest.leftForReview.length})`)}`);
|
|
132
|
+
for (const item of manifest.leftForReview) {
|
|
133
|
+
console.log(` ${chalk.yellow(' →')} ${chalk.dim(`[${item.category}]`)} ${item.summary}`);
|
|
134
|
+
console.log(` ${chalk.dim(item.reason)}`);
|
|
135
|
+
}
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Findings (synthesisDisplay) — split into new and previously reported
|
|
121
140
|
if (synthesis.synthesisDisplay && synthesis.synthesisDisplay.length > 0) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.log(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
const newFindings = synthesis.synthesisDisplay.filter((f) => f.isNew !== false);
|
|
142
|
+
const knownFindings = synthesis.synthesisDisplay.filter((f) => f.isNew === false);
|
|
143
|
+
// New findings — shown prominently
|
|
144
|
+
if (newFindings.length > 0) {
|
|
145
|
+
console.log(chalk.bold('\n Findings\n'));
|
|
146
|
+
for (let i = 0; i < newFindings.length; i++) {
|
|
147
|
+
const finding = newFindings[i];
|
|
148
|
+
const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
|
|
149
|
+
console.log(` ${chalk.yellow(`${i + 1}.`)} ${chalk.bold(finding.category)}${sourceTag}`);
|
|
150
|
+
console.log(` ${finding.summary}`);
|
|
151
|
+
if (finding.detail) {
|
|
152
|
+
const detailLines = finding.detail.split('\n');
|
|
153
|
+
for (const line of detailLines) {
|
|
154
|
+
console.log(` ${chalk.dim(line)}`);
|
|
155
|
+
}
|
|
133
156
|
}
|
|
157
|
+
console.log('');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Previously reported findings — dimmed
|
|
161
|
+
if (knownFindings.length > 0) {
|
|
162
|
+
console.log(chalk.dim(` Previously Reported (${knownFindings.length})\n`));
|
|
163
|
+
for (let i = 0; i < knownFindings.length; i++) {
|
|
164
|
+
const finding = knownFindings[i];
|
|
165
|
+
const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
|
|
166
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.dim(finding.category)}${sourceTag}`);
|
|
167
|
+
console.log(` ${chalk.dim(finding.summary)}`);
|
|
168
|
+
console.log('');
|
|
134
169
|
}
|
|
135
|
-
console.log('');
|
|
136
170
|
}
|
|
137
171
|
}
|
|
138
172
|
// Verified bugs
|
|
@@ -161,8 +195,8 @@ function printRichOutput(pr, synthesis) {
|
|
|
161
195
|
}
|
|
162
196
|
console.log('');
|
|
163
197
|
}
|
|
164
|
-
// Agent fix prompts —
|
|
165
|
-
const fixableFindings = (synthesis.synthesisDisplay || []).filter(f => f.agentFixPrompt);
|
|
198
|
+
// Agent fix prompts — only for new findings (known issues were already triaged)
|
|
199
|
+
const fixableFindings = (synthesis.synthesisDisplay || []).filter((f) => f.agentFixPrompt && f.isNew !== false);
|
|
166
200
|
if (fixableFindings.length > 0) {
|
|
167
201
|
console.log(chalk.bold(' Agent Fix Prompts'));
|
|
168
202
|
console.log(chalk.dim(' (Copy these to your coding agent to fix the issues)\n'));
|
|
@@ -181,7 +215,7 @@ function printRichOutput(pr, synthesis) {
|
|
|
181
215
|
console.log(` ${chalk.dim('Full review:')} ${chalk.cyan(reviewUrl)}`);
|
|
182
216
|
console.log('');
|
|
183
217
|
}
|
|
184
|
-
function printJsonOutput(pr, synthesis) {
|
|
218
|
+
function printJsonOutput(pr, synthesis, manifest) {
|
|
185
219
|
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
186
220
|
const output = {
|
|
187
221
|
owner: pr.owner,
|
|
@@ -192,12 +226,13 @@ function printJsonOutput(pr, synthesis) {
|
|
|
192
226
|
phase: synthesis.phase,
|
|
193
227
|
canAutoMerge: synthesis.reviewRecommendation?.canAutoMerge ?? false,
|
|
194
228
|
needsHumanReview: synthesis.needsHumanReview,
|
|
195
|
-
findings: (synthesis.synthesisDisplay || []).map(f => ({
|
|
229
|
+
findings: (synthesis.synthesisDisplay || []).map((f) => ({
|
|
196
230
|
category: f.category,
|
|
197
231
|
summary: f.summary,
|
|
198
232
|
detail: f.detail,
|
|
199
233
|
agentFixPrompt: f.agentFixPrompt || null,
|
|
200
234
|
source: f.source || null,
|
|
235
|
+
isNew: f.isNew ?? null,
|
|
201
236
|
})),
|
|
202
237
|
verifiedBugs: (synthesis.verifiedBugs || []).filter(b => b.verified).map(b => ({
|
|
203
238
|
title: b.originalTitle,
|
|
@@ -209,6 +244,11 @@ function printJsonOutput(pr, synthesis) {
|
|
|
209
244
|
humanReviewReasons: synthesis.humanReviewReasons || [],
|
|
210
245
|
verification: synthesis.verification || null,
|
|
211
246
|
verificationConfidence: synthesis.verificationConfidence || null,
|
|
247
|
+
autoFixManifest: manifest ? {
|
|
248
|
+
fixedItems: manifest.fixedItems,
|
|
249
|
+
leftForReview: manifest.leftForReview,
|
|
250
|
+
timestamp: manifest.timestamp,
|
|
251
|
+
} : null,
|
|
212
252
|
reviewUrl,
|
|
213
253
|
};
|
|
214
254
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -328,8 +368,11 @@ export async function triageCommand(identifier, options) {
|
|
|
328
368
|
}
|
|
329
369
|
if (!analysisReady)
|
|
330
370
|
return;
|
|
331
|
-
// Fetch the full rating synthesis
|
|
332
|
-
const synthesis = await
|
|
371
|
+
// Fetch the full rating synthesis and auto-fix manifest in parallel
|
|
372
|
+
const [synthesis, autoFixManifest] = await Promise.all([
|
|
373
|
+
fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
|
|
374
|
+
fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
|
|
375
|
+
]);
|
|
333
376
|
if (!synthesis) {
|
|
334
377
|
if (options.json) {
|
|
335
378
|
console.log(JSON.stringify({
|
|
@@ -346,9 +389,58 @@ export async function triageCommand(identifier, options) {
|
|
|
346
389
|
}
|
|
347
390
|
// Output
|
|
348
391
|
if (options.json) {
|
|
349
|
-
printJsonOutput(pr, synthesis);
|
|
392
|
+
printJsonOutput(pr, synthesis, autoFixManifest);
|
|
350
393
|
}
|
|
351
394
|
else {
|
|
352
|
-
printRichOutput(pr, synthesis);
|
|
395
|
+
printRichOutput(pr, synthesis, autoFixManifest);
|
|
396
|
+
}
|
|
397
|
+
// Record findings-seen marker (fire-and-forget — failure is non-fatal)
|
|
398
|
+
if (synthesis.synthesisDisplay && synthesis.synthesisDisplay.length > 0 && token) {
|
|
399
|
+
const findingKeys = synthesis.synthesisDisplay.map((f) => `${f.category}|${f.summary}`);
|
|
400
|
+
recordFindingsSeen(pr, token, findingKeys).catch((err) => {
|
|
401
|
+
// Non-blocking but logged — don't swallow silently
|
|
402
|
+
if (process.env.DEBUG) {
|
|
403
|
+
console.error(chalk.dim(` [debug] findings-seen write failed: ${err}`));
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Record that the user has seen analysis findings for this PR.
|
|
410
|
+
* Fire-and-forget: failure is silently ignored.
|
|
411
|
+
*/
|
|
412
|
+
async function recordFindingsSeen(pr, token, findingKeys) {
|
|
413
|
+
// Get PR head SHA
|
|
414
|
+
const ghResp = await fetch(`https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, {
|
|
415
|
+
headers: {
|
|
416
|
+
Authorization: `Bearer ${token}`,
|
|
417
|
+
Accept: 'application/vnd.github.v3+json',
|
|
418
|
+
'User-Agent': 'Haystack-CLI',
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
if (!ghResp.ok) {
|
|
422
|
+
throw new Error(`GitHub API returned ${ghResp.status}`);
|
|
423
|
+
}
|
|
424
|
+
const ghData = await ghResp.json();
|
|
425
|
+
const sha = ghData?.head?.sha;
|
|
426
|
+
if (typeof sha !== 'string' || !sha) {
|
|
427
|
+
throw new Error('Unexpected GitHub response: missing head.sha');
|
|
428
|
+
}
|
|
429
|
+
const resp = await fetch('https://haystackeditor.com/api/findings-seen', {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
headers: {
|
|
432
|
+
Authorization: `Bearer ${token}`,
|
|
433
|
+
'Content-Type': 'application/json',
|
|
434
|
+
'User-Agent': 'Haystack-CLI',
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
repoFullName: `${pr.owner}/${pr.repo}`,
|
|
438
|
+
prNumber: pr.prNumber,
|
|
439
|
+
commitSha: sha,
|
|
440
|
+
findingKeys,
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
if (!resp.ok) {
|
|
444
|
+
throw new Error(`findings-seen API returned ${resp.status}`);
|
|
353
445
|
}
|
|
354
446
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* npx @haystackeditor/cli triage 123 # View analysis results for a PR
|
|
16
16
|
* npx @haystackeditor/cli dismiss 123 # Dismiss findings for a PR
|
|
17
17
|
* npx @haystackeditor/cli mark-reviewed 123 # Mark review as not needed
|
|
18
|
+
* npx @haystackeditor/cli request-review 123 # Tag a PR as needing human review
|
|
18
19
|
* npx @haystackeditor/cli pr-status 123 # Show PR status in Haystack pipeline
|
|
19
20
|
* npx @haystackeditor/cli config # Manage preferences
|
|
20
21
|
*/
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* npx @haystackeditor/cli triage 123 # View analysis results for a PR
|
|
16
16
|
* npx @haystackeditor/cli dismiss 123 # Dismiss findings for a PR
|
|
17
17
|
* npx @haystackeditor/cli mark-reviewed 123 # Mark review as not needed
|
|
18
|
+
* npx @haystackeditor/cli request-review 123 # Tag a PR as needing human review
|
|
18
19
|
* npx @haystackeditor/cli pr-status 123 # Show PR status in Haystack pipeline
|
|
19
20
|
* npx @haystackeditor/cli config # Manage preferences
|
|
20
21
|
*/
|
|
@@ -31,13 +32,14 @@ import { installSessionHooks, sessionHooksStatus } from './commands/install-sess
|
|
|
31
32
|
import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } from './commands/policy.js';
|
|
32
33
|
import { triageCommand } from './commands/triage.js';
|
|
33
34
|
import { dismissCommand, markReviewedCommand, undismissCommand } from './commands/dismiss.js';
|
|
35
|
+
import { requestReviewCommand } from './commands/request-review.js';
|
|
34
36
|
import { prStatusCommand } from './commands/pr-status.js';
|
|
35
37
|
import { setupCommand } from './commands/setup.js';
|
|
36
38
|
const program = new Command();
|
|
37
39
|
program
|
|
38
40
|
.name('haystack')
|
|
39
41
|
.description('Haystack CLI — automated PR review, triage, and merge queue')
|
|
40
|
-
.version('0.
|
|
42
|
+
.version('0.11.0');
|
|
41
43
|
program
|
|
42
44
|
.command('init')
|
|
43
45
|
.description('Create .haystack.json configuration')
|
|
@@ -98,7 +100,7 @@ program
|
|
|
98
100
|
.addHelpText('after', `
|
|
99
101
|
This command is designed for AI coding agents to submit PRs.
|
|
100
102
|
|
|
101
|
-
1. Runs pre-PR triage (code review, rules,
|
|
103
|
+
1. Runs pre-PR triage (code review, rules, instruction drift) via sub-agents
|
|
102
104
|
2. Pushes the current branch to origin
|
|
103
105
|
3. Creates a pull request on GitHub
|
|
104
106
|
4. Waits for Haystack analysis results (triggered via GitHub App webhook)
|
|
@@ -107,7 +109,7 @@ Pre-PR Triage:
|
|
|
107
109
|
Before creating the PR, haystack spawns parallel sub-agents to check for:
|
|
108
110
|
• Code review bugs (logic errors, null crashes, security issues)
|
|
109
111
|
• Rule violations (from .haystack/pr-rules.yml)
|
|
110
|
-
•
|
|
112
|
+
• Instruction drift (AI agent deviations from user instructions)
|
|
111
113
|
|
|
112
114
|
Use --force to skip triage entirely.
|
|
113
115
|
Use --no-wait to skip waiting for analysis results.
|
|
@@ -247,6 +249,29 @@ Examples:
|
|
|
247
249
|
haystack undismiss acme/widgets#99 # Undo for specific repo
|
|
248
250
|
`)
|
|
249
251
|
.action(undismissCommand);
|
|
252
|
+
program
|
|
253
|
+
.command('request-review <pr> [reviewer]')
|
|
254
|
+
.description('Tag a PR as needing human review')
|
|
255
|
+
.addHelpText('after', `
|
|
256
|
+
Tag any PR with "needs human review", even after it was created.
|
|
257
|
+
Adds the haystack:needs-review label to the PR, moving it from
|
|
258
|
+
"Good to Merge" to the "Needs Assignment" queue in the feed.
|
|
259
|
+
|
|
260
|
+
Optionally specify a reviewer to request review from a specific
|
|
261
|
+
GitHub user.
|
|
262
|
+
|
|
263
|
+
PR identifier formats:
|
|
264
|
+
123 PR number (uses current repo)
|
|
265
|
+
#123 PR number with hash
|
|
266
|
+
owner/repo#123 Fully qualified
|
|
267
|
+
https://github.com/owner/repo/pull/123 GitHub URL
|
|
268
|
+
|
|
269
|
+
Examples:
|
|
270
|
+
haystack request-review 42 # Tag PR #42 for human review
|
|
271
|
+
haystack request-review 42 octocat # Tag and request review from octocat
|
|
272
|
+
haystack request-review acme/widgets#99 # Specific repo
|
|
273
|
+
`)
|
|
274
|
+
.action(requestReviewCommand);
|
|
250
275
|
program
|
|
251
276
|
.command('pr-status <pr>')
|
|
252
277
|
.description('Show the current Haystack status of a PR')
|
|
@@ -317,25 +342,25 @@ config
|
|
|
317
342
|
.description('Configure which AI reviewers the merge queue waits for')
|
|
318
343
|
.addHelpText('after', `
|
|
319
344
|
Actions:
|
|
320
|
-
list Show
|
|
321
|
-
add <names> Add one or more
|
|
322
|
-
remove <names> Remove one or more
|
|
323
|
-
clear Remove all
|
|
345
|
+
list Show configured reviewer bots (default)
|
|
346
|
+
add <names> Add one or more reviewer bots
|
|
347
|
+
remove <names> Remove one or more reviewer bots
|
|
348
|
+
clear Remove all reviewer bots
|
|
324
349
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
wait for these reviewers to post before starting the
|
|
328
|
-
grace period.
|
|
350
|
+
Accepts friendly names (cursor, coderabbit) or exact
|
|
351
|
+
GitHub bot usernames (cursor-bugbot[bot]).
|
|
329
352
|
|
|
330
|
-
|
|
331
|
-
|
|
353
|
+
Stored in .haystack.json under merge_queue.wait_for_reviewer.bots.
|
|
354
|
+
The merge queue will block merging until ALL configured
|
|
355
|
+
bots have posted a review or comment on the PR.
|
|
332
356
|
|
|
333
357
|
Examples:
|
|
334
|
-
haystack config wait-for-reviewers
|
|
335
|
-
haystack config wait-for-reviewers add cursor
|
|
336
|
-
haystack config wait-for-reviewers add cursor coderabbit
|
|
337
|
-
haystack config wait-for-reviewers
|
|
338
|
-
haystack config wait-for-reviewers
|
|
358
|
+
haystack config wait-for-reviewers # Show status
|
|
359
|
+
haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
|
|
360
|
+
haystack config wait-for-reviewers add cursor coderabbit # Add multiple
|
|
361
|
+
haystack config wait-for-reviewers add cursor-bugbot[bot] # Raw bot username
|
|
362
|
+
haystack config wait-for-reviewers remove cursor # Stop waiting
|
|
363
|
+
haystack config wait-for-reviewers clear # Wait for none
|
|
339
364
|
`)
|
|
340
365
|
.action(handleWaitForReviewers);
|
|
341
366
|
// Skills subcommands
|