@haystackeditor/cli 0.11.0 → 0.12.1

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.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * install-session-hooks — Wire up session-start hooks for coding CLIs.
3
3
  *
4
- * When a CLI session starts, `haystack check-pending --hook` runs automatically
5
- * and shows the user whether their pending PRs are "Good to merge" or "Need input".
4
+ * When a CLI session starts, `haystack triage --hook` runs automatically
5
+ * and shows the user whether their last submitted PR is "Good to merge" or
6
+ * "Needs input", with the Haystack rating and auto-fixer status.
6
7
  *
7
8
  * Supported CLIs:
8
9
  * - Claude Code: Native SessionStart hook in .claude/settings.json
@@ -41,7 +42,7 @@ function detectInstalledCLIs() {
41
42
  // ============================================================================
42
43
  // Claude Code: Native SessionStart hook
43
44
  // ============================================================================
44
- const HAYSTACK_HOOK_COMMAND = 'haystack check-pending --hook';
45
+ const HAYSTACK_HOOK_COMMAND = 'haystack triage --hook';
45
46
  function installClaudeHook(gitRoot) {
46
47
  const settingsDir = join(gitRoot, '.claude');
47
48
  const settingsPath = join(settingsDir, 'settings.json');
@@ -61,13 +62,28 @@ function installClaudeHook(gitRoot) {
61
62
  // Ensure SessionStart array exists
62
63
  const sessionStart = (hooks.SessionStart || []);
63
64
  hooks.SessionStart = sessionStart;
64
- // Check if haystack hook already installed
65
- const alreadyInstalled = sessionStart.some(entry => entry.hooks?.some(h => h.command?.includes('haystack check-pending')));
66
- if (alreadyInstalled) {
67
- console.log(chalk.dim(' Claude Code hook already installed.'));
65
+ // Check if haystack hook already installed (current or legacy)
66
+ const isHaystackHook = (h) => h.command?.includes('haystack triage') || h.command?.includes('haystack check-pending');
67
+ const existingIndex = sessionStart.findIndex(entry => entry.hooks?.some(h => isHaystackHook(h)));
68
+ if (existingIndex !== -1) {
69
+ const entry = sessionStart[existingIndex];
70
+ const isLegacy = entry.hooks?.some(h => h.command?.includes('haystack check-pending'));
71
+ if (isLegacy) {
72
+ // Migrate legacy hook in-place
73
+ for (const h of entry.hooks || []) {
74
+ if (h.command?.includes('haystack check-pending')) {
75
+ h.command = HAYSTACK_HOOK_COMMAND;
76
+ }
77
+ }
78
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
79
+ console.log(chalk.green(' Claude Code hook migrated from check-pending to triage.'));
80
+ }
81
+ else {
82
+ console.log(chalk.dim(' Claude Code hook already installed.'));
83
+ }
68
84
  return true;
69
85
  }
70
- // Add the hook
86
+ // Add the hook (fresh install)
71
87
  sessionStart.push({
72
88
  matcher: '',
73
89
  hooks: [
@@ -92,7 +108,7 @@ function checkClaudeHook(gitRoot) {
92
108
  try {
93
109
  const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
94
110
  const sessionStart = settings?.hooks?.SessionStart || [];
95
- return sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes('haystack check-pending')));
111
+ return sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes('haystack triage') || h.command?.includes('haystack check-pending')));
96
112
  }
97
113
  catch {
98
114
  return false;
@@ -109,7 +125,7 @@ ${CODEX_HOOK_MARKER}
109
125
  When starting a new session, run this command first to check for pending PR analysis results:
110
126
 
111
127
  \`\`\`bash
112
- haystack check-pending --hook
128
+ haystack triage --hook
113
129
  \`\`\`
114
130
 
115
131
  This shows whether your submitted PRs are "Good to merge" or "Need your input".
@@ -157,7 +173,7 @@ ${GEMINI_HOOK_MARKER}
157
173
  When starting a new session, run this command first to check for pending PR analysis results:
158
174
 
159
175
  \`\`\`bash
160
- haystack check-pending --hook
176
+ haystack triage --hook
161
177
  \`\`\`
162
178
 
163
179
  This shows whether your submitted PRs are "Good to merge" or "Need your input".
@@ -13,4 +13,9 @@
13
13
  export interface PRStatusOptions {
14
14
  json?: boolean;
15
15
  }
16
+ export declare const BUCKET_DISPLAY: Record<string, {
17
+ label: string;
18
+ color: (s: string) => string;
19
+ icon: string;
20
+ }>;
16
21
  export declare function prStatusCommand(identifier: string, options: PRStatusOptions): Promise<void>;
@@ -46,9 +46,9 @@ function parsePRIdentifier(identifier) {
46
46
  // ============================================================================
47
47
  // Display helpers
48
48
  // ============================================================================
49
- const BUCKET_DISPLAY = {
49
+ export const BUCKET_DISPLAY = {
50
50
  'analyzing': { label: 'Analyzing', color: chalk.blue, icon: '...' },
51
- 'auto-fixing': { label: 'Auto-fixing', color: chalk.blue, icon: '>>>' },
51
+ 'auto-fixing': { label: 'Auto-fixing (alpha)', color: chalk.blue, icon: '>>>' },
52
52
  'good-to-merge': { label: 'Good to Merge', color: chalk.green, icon: '[+]' },
53
53
  'failed-when-merging': { label: 'Failed When Merging', color: chalk.red, icon: '[!]' },
54
54
  'issues': { label: 'Issues Found', color: chalk.yellow, icon: '[?]' },
@@ -509,7 +509,7 @@ export async function setupCommand() {
509
509
  // =============================================================================
510
510
  // Step 7: Install Entire CLI for session tracking
511
511
  // =============================================================================
512
- const ENTIRE_VERSION = 'v0.5.3-haystack.1';
512
+ const ENTIRE_VERSION = 'v0.5.3-haystack.2';
513
513
  const ENTIRE_RELEASE_BASE = `https://github.com/haystackeditor/cli/releases/download/${ENTIRE_VERSION}`;
514
514
  function getEntireBinaryName() {
515
515
  const platform = process.platform === 'darwin' ? 'darwin'
@@ -591,7 +591,7 @@ async function installEntireBinary() {
591
591
  }
592
592
  }
593
593
  /**
594
- * Write .entire/settings.json with skip_raw_transcript via the GitHub API.
594
+ * Write .entire/settings.json via the GitHub API.
595
595
  * selectedRepos are GitHub full names (owner/repo), not local paths.
596
596
  */
597
597
  async function configureEntireSettingsViaAPI(repoFullName, token) {
@@ -606,12 +606,14 @@ async function configureEntireSettingsViaAPI(repoFullName, token) {
606
606
  // Fetch existing .entire/settings.json (if any)
607
607
  let existingSha = null;
608
608
  let settings = { enabled: true };
609
+ let originalContent = '';
609
610
  const getResp = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/contents/${filePath}`, { headers });
610
611
  if (getResp.ok) {
611
612
  const data = (await getResp.json());
612
613
  existingSha = data.sha;
614
+ originalContent = Buffer.from(data.content, 'base64').toString('utf-8');
613
615
  try {
614
- settings = JSON.parse(Buffer.from(data.content, 'base64').toString('utf-8'));
616
+ settings = JSON.parse(originalContent);
615
617
  }
616
618
  catch {
617
619
  // Malformed JSON — keep sha for update but use fresh defaults
@@ -630,13 +632,22 @@ async function configureEntireSettingsViaAPI(repoFullName, token) {
630
632
  throw error;
631
633
  }
632
634
  // 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');
635
+ // Remove legacy skip_raw_transcript if present (v0.5.3-haystack.2+ compacts automatically)
636
+ if (settings.strategy_options && typeof settings.strategy_options === 'object') {
637
+ const strategyOptions = settings.strategy_options;
638
+ delete strategyOptions.skip_raw_transcript;
639
+ if (Object.keys(strategyOptions).length === 0) {
640
+ delete settings.strategy_options;
641
+ }
642
+ }
643
+ const updatedContent = JSON.stringify(settings, null, 2) + '\n';
644
+ // Skip write if nothing changed (avoids no-op commits)
645
+ if (existingSha && updatedContent === originalContent) {
646
+ return;
647
+ }
648
+ const content = Buffer.from(updatedContent).toString('base64');
638
649
  const body = {
639
- message: 'chore: enable skip_raw_transcript for Entire CLI\n\nPrevents committing raw 50MB session transcripts to git.',
650
+ message: 'chore: configure Entire CLI settings',
640
651
  content,
641
652
  };
642
653
  if (existingSha)
@@ -720,7 +731,7 @@ async function stepInstallEntire(selectedRepos, token) {
720
731
  }
721
732
  }
722
733
  if (configured > 0) {
723
- console.log(chalk.green(`\n ✓ Session tracking configured on ${configured} repo(s) (raw transcripts excluded)\n`));
734
+ console.log(chalk.green(`\n ✓ Session tracking configured on ${configured} repo(s) (transcripts auto-compacted)\n`));
724
735
  trackSetupEvent('entire_configured', { configured, total: selectedRepos.length });
725
736
  }
726
737
  else {
@@ -19,5 +19,6 @@ export interface SubmitOptions {
19
19
  force?: boolean;
20
20
  wait?: boolean;
21
21
  autoFix?: boolean;
22
+ autoMerge?: boolean;
22
23
  }
23
24
  export declare function submitCommand(options: SubmitOptions): Promise<void>;
@@ -16,7 +16,6 @@ import { createPullRequest, findExistingPR, requireGitHubAuth, requestReviewers,
16
16
  import { runTriage } from '../triage/runner.js';
17
17
  import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
18
18
  import { savePendingSubmit, updatePendingSubmit } from '../utils/pending-state.js';
19
- import { isAutoMergeEnabled } from './config.js';
20
19
  import { ensureHaystackLabels, addLabelsToIssue, } from '../utils/github-api.js';
21
20
  // ============================================================================
22
21
  // Helpers
@@ -269,14 +268,7 @@ export async function submitCommand(options) {
269
268
  // -------------------------------------------------------------------------
270
269
  // Step 6: Apply auto-merge label if enabled
271
270
  // -------------------------------------------------------------------------
272
- let autoMergeEnabled = false;
273
- try {
274
- autoMergeEnabled = await isAutoMergeEnabled();
275
- }
276
- catch {
277
- // Preference check failure should not block submission
278
- }
279
- if (autoMergeEnabled) {
271
+ if (options.autoMerge) {
280
272
  try {
281
273
  await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge']);
282
274
  await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge']);
@@ -293,7 +285,7 @@ export async function submitCommand(options) {
293
285
  try {
294
286
  await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix']);
295
287
  await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix']);
296
- console.log(chalk.green('✓ Auto-fix enabled for this PR'));
288
+ console.log(chalk.green('✓ Auto-fix (alpha) enabled for this PR'));
297
289
  }
298
290
  catch (err) {
299
291
  console.log(chalk.yellow(`⚠ Could not set auto-fix label: ${err.message}`));
@@ -328,11 +320,11 @@ export async function submitCommand(options) {
328
320
  console.log(` ${chalk.dim('URL:')} ${chalk.cyan(prUrl)}`);
329
321
  console.log(` ${chalk.dim('Title:')} ${options.title || getLastCommitMessage()}`);
330
322
  console.log(` ${chalk.dim('Branch:')} ${currentBranch} → ${baseBranch}`);
331
- if (autoMergeEnabled) {
323
+ if (options.autoMerge) {
332
324
  console.log(` ${chalk.dim('Merge:')} ${chalk.green('Auto-merge if safe')}`);
333
325
  }
334
326
  if (options.autoFix) {
335
- console.log(` ${chalk.dim('Fix:')} ${chalk.cyan('Auto-fix issues before Feed')}`);
327
+ console.log(` ${chalk.dim('Fix:')} ${chalk.cyan('Auto-fix alpha enabled')}`);
336
328
  }
337
329
  if (options.review) {
338
330
  const reviewer = typeof options.review === 'string' ? options.review : 'needs assignment';
@@ -343,7 +335,7 @@ export async function submitCommand(options) {
343
335
  // Step 9: Wait for Haystack analysis (triggered via GitHub App webhook)
344
336
  // -------------------------------------------------------------------------
345
337
  console.log(chalk.dim('Haystack analysis triggered via GitHub App webhook.'));
346
- // Save pending state (so check-pending works even if we disconnect)
338
+ // Save pending state (so `haystack triage` works even if we disconnect)
347
339
  const pendingState = {
348
340
  version: 1,
349
341
  prNumber,
@@ -359,7 +351,7 @@ export async function submitCommand(options) {
359
351
  // If --no-wait or non-interactive, exit here
360
352
  if (options.wait === false || !process.stdin.isTTY) {
361
353
  console.log(chalk.dim('\nAnalysis running in background.'));
362
- console.log(chalk.dim('Check results later with: ') + chalk.cyan('haystack check-pending\n'));
354
+ console.log(chalk.dim('Check results later with: ') + chalk.cyan('haystack triage\n'));
363
355
  return;
364
356
  }
365
357
  // Wait for analysis inline by polling the result endpoint
@@ -369,7 +361,7 @@ export async function submitCommand(options) {
369
361
  const sigintHandler = () => {
370
362
  interrupted = true;
371
363
  console.log(chalk.dim('\n\nAnalysis still running. Check later with:'));
372
- console.log(chalk.cyan(' haystack check-pending\n'));
364
+ console.log(chalk.cyan(' haystack triage\n'));
373
365
  process.exit(0);
374
366
  };
375
367
  process.on('SIGINT', sigintHandler);
@@ -407,9 +399,10 @@ export async function submitCommand(options) {
407
399
  else if (options.autoFix) {
408
400
  // Auto-fix flow: agent fixes straightforward issues in the background.
409
401
  // Issues appear in Feed immediately — user can review while agent works.
410
- console.log(chalk.cyan.bold(' ⚙ Auto-fix triggered\n'));
402
+ console.log(chalk.cyan.bold(' ⚙ Auto-fix triggered (alpha)\n'));
411
403
  console.log(chalk.dim(` ${verdict.summary}\n`));
412
- console.log(chalk.dim(' Coding agent is fixing straightforward issues in the background.'));
404
+ console.log(chalk.dim(' Coding agent is fixing straightforward mechanical issues in the background.'));
405
+ console.log(chalk.dim(' Auto-fix is currently alpha.'));
413
406
  console.log(chalk.dim(' Issues that need your judgment are in the Feed now.\n'));
414
407
  const reviewUrl = `https://haystackeditor.com/review/${remoteInfo.owner}/${remoteInfo.repo}/${prNumber}`;
415
408
  console.log(` ${chalk.dim('Feed:')} ${chalk.cyan(reviewUrl)}\n`);
@@ -451,6 +444,6 @@ export async function submitCommand(options) {
451
444
  process.removeListener('SIGINT', sigintHandler);
452
445
  if (Date.now() >= deadline && !interrupted) {
453
446
  console.log(chalk.yellow('\n\n Timed out waiting for analysis.'));
454
- console.log(chalk.dim(' Check results later with: ') + chalk.cyan('haystack check-pending\n'));
447
+ console.log(chalk.dim(' Check results later with: ') + chalk.cyan('haystack triage\n'));
455
448
  }
456
449
  }
@@ -1,16 +1,28 @@
1
1
  /**
2
- * triage command — View Haystack analysis results for any PR.
2
+ * triage command — Show Haystack analysis results for a PR.
3
3
  *
4
- * Fetches the full rating synthesis (same data shown on the Haystack feed)
5
- * and displays verdict, findings, verified bugs, and agent fix prompts.
4
+ * Designed for coding agents to consume. Shows two things:
5
+ * 1. What the auto-fixer is already handling (so the agent doesn't duplicate work)
6
+ * 2. Findings with attribution and fix prompts (so the agent knows what to fix)
7
+ *
8
+ * When called without a PR identifier, reads the last submitted PR from
9
+ * `.haystack/pending-submit.json` (written by `haystack submit`).
6
10
  *
7
11
  * Accepts a PR identifier in several formats:
12
+ * haystack triage # Last submitted PR
8
13
  * haystack triage 123 # Uses current repo's owner/repo
9
14
  * haystack triage owner/repo#123 # Fully qualified
10
15
  * haystack triage https://github.com/owner/repo/pull/123
16
+ *
17
+ * Output modes:
18
+ * --hook Minimal single-line for session-start hooks
19
+ * --json Machine-readable JSON
20
+ * (default) Rich terminal output
11
21
  */
12
22
  export interface TriageOptions {
13
23
  json?: boolean;
14
24
  wait?: boolean;
25
+ hook?: boolean;
26
+ clear?: boolean;
15
27
  }
16
- export declare function triageCommand(identifier: string, options: TriageOptions): Promise<void>;
28
+ export declare function triageCommand(identifier: string | undefined, options: TriageOptions): Promise<void>;