@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.
@@ -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
  }
@@ -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 'Intent Drift';
54
+ case 'intent-drift': return 'Instruction Drift';
55
55
  default: return checker;
56
56
  }
57
57
  }
@@ -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
- // Findings (synthesisDisplay) — the main actionable content
120
+ // Auto-fix manifestshow 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
- console.log(chalk.bold('\n Findings\n'));
123
- for (let i = 0; i < synthesis.synthesisDisplay.length; i++) {
124
- const finding = synthesis.synthesisDisplay[i];
125
- const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
126
- console.log(` ${chalk.yellow(`${i + 1}.`)} ${chalk.bold(finding.category)}${sourceTag}`);
127
- console.log(` ${finding.summary}`);
128
- if (finding.detail) {
129
- // Indent multi-line detail
130
- const detailLines = finding.detail.split('\n');
131
- for (const line of detailLines) {
132
- console.log(` ${chalk.dim(line)}`);
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 — the key actionable output for coding agents
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 fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token);
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.10.2');
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, intent drift) via sub-agents
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
- Intent drift (AI agent deviations from user instructions)
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 current expected reviewers (default)
321
- add <names> Add one or more AI reviewer sources
322
- remove <names> Remove one or more AI reviewer sources
323
- clear Remove all expected reviewers
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
- AI reviewer sources are stored in .haystack.json under
326
- babysitter.expected_ai_reviewers. The merge queue will
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
- Run \`haystack config wait-for-reviewers list\` to see
331
- all available reviewer sources and which are enabled.
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 # Show status
335
- haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
336
- haystack config wait-for-reviewers add cursor coderabbit # Add multiple
337
- haystack config wait-for-reviewers remove cursor # Stop waiting for Cursor
338
- haystack config wait-for-reviewers clear # Wait for none
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