@haystackeditor/cli 0.8.1 → 0.9.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.
Files changed (55) hide show
  1. package/README.md +46 -0
  2. package/dist/assets/hooks/llm-rules-template.md +21 -0
  3. package/dist/assets/hooks/scripts/pre-push.sh +20 -0
  4. package/dist/assets/skills/prepare-haystack.md +323 -0
  5. package/dist/assets/skills/secrets.md +164 -0
  6. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  7. package/dist/assets/skills/setup-haystack.md +639 -0
  8. package/dist/assets/skills/submit.md +154 -0
  9. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  10. package/dist/assets/templates/haystack.yml +193 -0
  11. package/dist/commands/check-pending.d.ts +19 -0
  12. package/dist/commands/check-pending.js +217 -0
  13. package/dist/commands/config.d.ts +13 -21
  14. package/dist/commands/config.js +278 -92
  15. package/dist/commands/install-session-hooks.d.ts +16 -0
  16. package/dist/commands/install-session-hooks.js +302 -0
  17. package/dist/commands/login.js +1 -1
  18. package/dist/commands/policy.d.ts +31 -0
  19. package/dist/commands/policy.js +365 -0
  20. package/dist/commands/skills.d.ts +2 -2
  21. package/dist/commands/skills.js +51 -186
  22. package/dist/commands/submit.d.ts +22 -0
  23. package/dist/commands/submit.js +428 -0
  24. package/dist/commands/triage.d.ts +16 -0
  25. package/dist/commands/triage.js +354 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +232 -2
  28. package/dist/tools/detect.d.ts +50 -0
  29. package/dist/tools/detect.js +853 -0
  30. package/dist/tools/fixtures.d.ts +38 -0
  31. package/dist/tools/fixtures.js +199 -0
  32. package/dist/tools/setup.d.ts +43 -0
  33. package/dist/tools/setup.js +597 -0
  34. package/dist/triage/prompts.d.ts +31 -0
  35. package/dist/triage/prompts.js +266 -0
  36. package/dist/triage/runner.d.ts +21 -0
  37. package/dist/triage/runner.js +325 -0
  38. package/dist/triage/traces.d.ts +20 -0
  39. package/dist/triage/traces.js +305 -0
  40. package/dist/triage/types.d.ts +47 -0
  41. package/dist/triage/types.js +7 -0
  42. package/dist/types.d.ts +1387 -191
  43. package/dist/types.js +254 -2
  44. package/dist/utils/analysis-api.d.ts +108 -0
  45. package/dist/utils/analysis-api.js +194 -0
  46. package/dist/utils/config.js +1 -1
  47. package/dist/utils/git.d.ts +80 -0
  48. package/dist/utils/git.js +302 -0
  49. package/dist/utils/github-api.d.ts +83 -0
  50. package/dist/utils/github-api.js +266 -0
  51. package/dist/utils/pending-state.d.ts +38 -0
  52. package/dist/utils/pending-state.js +86 -0
  53. package/dist/utils/secrets.js +3 -3
  54. package/dist/utils/skill.js +257 -0
  55. package/package.json +4 -2
@@ -0,0 +1,354 @@
1
+ /**
2
+ * triage command — View Haystack analysis results for any PR.
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.
6
+ *
7
+ * Accepts a PR identifier in several formats:
8
+ * haystack triage 123 # Uses current repo's owner/repo
9
+ * haystack triage owner/repo#123 # Fully qualified
10
+ * haystack triage https://github.com/owner/repo/pull/123
11
+ */
12
+ import chalk from 'chalk';
13
+ import { checkAnalysisReady, fetchRatingSynthesis } from '../utils/analysis-api.js';
14
+ import { parseRemoteUrl } from '../utils/git.js';
15
+ import { loadToken } from './login.js';
16
+ // ============================================================================
17
+ // PR identifier parsing
18
+ // ============================================================================
19
+ /**
20
+ * Parse a PR identifier from various formats:
21
+ * 123 -> uses current repo
22
+ * #123 -> uses current repo
23
+ * owner/repo#123 -> fully qualified
24
+ * https://github.com/owner/repo/pull/123 -> GitHub URL
25
+ */
26
+ function parsePRIdentifier(identifier) {
27
+ // GitHub URL: https://github.com/owner/repo/pull/123
28
+ const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
29
+ if (urlMatch) {
30
+ return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
31
+ }
32
+ // Fully qualified: owner/repo#123
33
+ const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
34
+ if (qualifiedMatch) {
35
+ return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
36
+ }
37
+ // Bare number: 123 or #123
38
+ const numberMatch = identifier.match(/^#?(\d+)$/);
39
+ if (numberMatch) {
40
+ const prNumber = parseInt(numberMatch[1], 10);
41
+ // Infer owner/repo from git remote
42
+ try {
43
+ const remote = parseRemoteUrl('origin');
44
+ return { owner: remote.owner, repo: remote.repo, prNumber };
45
+ }
46
+ catch {
47
+ throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
48
+ `Use the full format: haystack triage owner/repo#${prNumber}`);
49
+ }
50
+ }
51
+ throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
52
+ `Accepted formats:\n` +
53
+ ` haystack triage 123\n` +
54
+ ` haystack triage owner/repo#123\n` +
55
+ ` haystack triage https://github.com/owner/repo/pull/123`);
56
+ }
57
+ // ============================================================================
58
+ // Output formatting
59
+ // ============================================================================
60
+ function ratingLabel(rating) {
61
+ switch (rating) {
62
+ case 5: return chalk.green.bold('5/5') + chalk.green(' — Safe to merge');
63
+ case 4: return chalk.green('4/5') + chalk.dim(' — Low risk');
64
+ case 3: return chalk.yellow('3/5') + chalk.dim(' — Moderate risk');
65
+ case 2: return chalk.red('2/5') + chalk.dim(' — High risk');
66
+ case 1: return chalk.red.bold('1/5') + chalk.red(' — Critical issues');
67
+ default: return chalk.dim(`${rating}/5`);
68
+ }
69
+ }
70
+ function verdictLabel(verdict) {
71
+ switch (verdict) {
72
+ case 'clean': return chalk.green.bold('Clean');
73
+ case 'has-issues': return chalk.yellow.bold('Has Issues');
74
+ default: return chalk.dim('Unknown');
75
+ }
76
+ }
77
+ function severityIcon(severity) {
78
+ switch (severity) {
79
+ case 'critical': return chalk.red.bold('✗✗');
80
+ case 'high': return chalk.red('✗');
81
+ case 'medium': return chalk.yellow('!');
82
+ case 'low': return chalk.blue('·');
83
+ default: return chalk.dim('-');
84
+ }
85
+ }
86
+ function printRichOutput(pr, synthesis) {
87
+ const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
88
+ console.log(chalk.bold('\n' + '━'.repeat(60)));
89
+ console.log(chalk.bold(` Haystack Analysis: ${pr.owner}/${pr.repo}#${pr.prNumber}`));
90
+ console.log(chalk.bold('━'.repeat(60) + '\n'));
91
+ // Rating & verdict
92
+ console.log(` ${chalk.dim('Rating:')} ${ratingLabel(synthesis.haystackRating)}`);
93
+ console.log(` ${chalk.dim('Verdict:')} ${verdictLabel(synthesis.analysisVerdict)}`);
94
+ if (synthesis.phase) {
95
+ const phaseLabel = synthesis.phase === 2 ? 'Final (post-verification)' : 'Preliminary (pre-verification)';
96
+ console.log(` ${chalk.dim('Phase:')} ${chalk.dim(phaseLabel)}`);
97
+ }
98
+ if (synthesis.verificationConfidence) {
99
+ const conf = synthesis.verificationConfidence;
100
+ const levelColor = conf.level === 'high' ? chalk.green : conf.level === 'medium' ? chalk.yellow : chalk.red;
101
+ console.log(` ${chalk.dim('Verified:')} ${levelColor(conf.level)} — ${chalk.dim(conf.summary)}`);
102
+ }
103
+ if (synthesis.reviewRecommendation?.canAutoMerge) {
104
+ console.log(` ${chalk.dim('Merge:')} ${chalk.green('Safe to auto-merge')}`);
105
+ }
106
+ // Verification results (build/test/lint)
107
+ if (synthesis.verification) {
108
+ const v = synthesis.verification;
109
+ const parts = [];
110
+ if (v.build)
111
+ parts.push(v.build.success ? chalk.green('build ✓') : chalk.red('build ✗'));
112
+ if (v.test)
113
+ parts.push(v.test.success ? chalk.green('test ✓') : chalk.red('test ✗'));
114
+ if (v.lint)
115
+ parts.push(v.lint.success ? chalk.green('lint ✓') : chalk.red('lint ✗'));
116
+ if (parts.length > 0) {
117
+ console.log(` ${chalk.dim('Checks:')} ${parts.join(' ')}`);
118
+ }
119
+ }
120
+ // Findings (synthesisDisplay) — the main actionable content
121
+ 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)}`);
133
+ }
134
+ }
135
+ console.log('');
136
+ }
137
+ }
138
+ // Verified bugs
139
+ const confirmedBugs = (synthesis.verifiedBugs || []).filter(b => b.verified);
140
+ if (confirmedBugs.length > 0) {
141
+ console.log(chalk.bold(' Verified Bugs\n'));
142
+ for (const bug of confirmedBugs) {
143
+ const sourceTag = bug.source ? chalk.dim(` (via ${bug.source.type}${bug.source.author ? `: ${bug.source.author}` : ''})`) : '';
144
+ console.log(` ${severityIcon(bug.assessedSeverity)} ${chalk.bold(bug.originalTitle)}${sourceTag}`);
145
+ console.log(` ${chalk.dim('Severity:')} ${bug.assessedSeverity}${bug.likelihood ? `, ${bug.likelihood} occurrence` : ''}`);
146
+ console.log(` ${bug.rationale}`);
147
+ console.log('');
148
+ }
149
+ }
150
+ // Human review reasons
151
+ if (synthesis.humanReviewReasons && synthesis.humanReviewReasons.length > 0) {
152
+ console.log(chalk.bold(' Human Review Required\n'));
153
+ for (const reason of synthesis.humanReviewReasons) {
154
+ const icon = reason.severity === 'high' || reason.severity === 'critical'
155
+ ? chalk.red('!')
156
+ : chalk.yellow('·');
157
+ console.log(` ${icon} ${reason.reason}`);
158
+ if (reason.source) {
159
+ console.log(` ${chalk.dim(`Source: ${reason.source}`)}`);
160
+ }
161
+ }
162
+ console.log('');
163
+ }
164
+ // Agent fix prompts — the key actionable output for coding agents
165
+ const fixableFindings = (synthesis.synthesisDisplay || []).filter(f => f.agentFixPrompt);
166
+ if (fixableFindings.length > 0) {
167
+ console.log(chalk.bold(' Agent Fix Prompts'));
168
+ console.log(chalk.dim(' (Copy these to your coding agent to fix the issues)\n'));
169
+ for (let i = 0; i < fixableFindings.length; i++) {
170
+ const finding = fixableFindings[i];
171
+ console.log(chalk.cyan(` --- Fix ${i + 1}: ${finding.category} ---`));
172
+ console.log(` ${finding.agentFixPrompt}`);
173
+ console.log('');
174
+ }
175
+ }
176
+ // Verification impact
177
+ if (synthesis.verificationImpact) {
178
+ console.log(` ${chalk.dim('Verification impact:')} ${synthesis.verificationImpact}`);
179
+ console.log('');
180
+ }
181
+ console.log(` ${chalk.dim('Full review:')} ${chalk.cyan(reviewUrl)}`);
182
+ console.log('');
183
+ }
184
+ function printJsonOutput(pr, synthesis) {
185
+ const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
186
+ const output = {
187
+ owner: pr.owner,
188
+ repo: pr.repo,
189
+ prNumber: pr.prNumber,
190
+ haystackRating: synthesis.haystackRating,
191
+ analysisVerdict: synthesis.analysisVerdict,
192
+ phase: synthesis.phase,
193
+ canAutoMerge: synthesis.reviewRecommendation?.canAutoMerge ?? false,
194
+ needsHumanReview: synthesis.needsHumanReview,
195
+ findings: (synthesis.synthesisDisplay || []).map(f => ({
196
+ category: f.category,
197
+ summary: f.summary,
198
+ detail: f.detail,
199
+ agentFixPrompt: f.agentFixPrompt || null,
200
+ source: f.source || null,
201
+ })),
202
+ verifiedBugs: (synthesis.verifiedBugs || []).filter(b => b.verified).map(b => ({
203
+ title: b.originalTitle,
204
+ severity: b.assessedSeverity,
205
+ likelihood: b.likelihood,
206
+ rationale: b.rationale,
207
+ source: b.source || null,
208
+ })),
209
+ humanReviewReasons: synthesis.humanReviewReasons || [],
210
+ verification: synthesis.verification || null,
211
+ verificationConfidence: synthesis.verificationConfidence || null,
212
+ reviewUrl,
213
+ };
214
+ console.log(JSON.stringify(output, null, 2));
215
+ }
216
+ // ============================================================================
217
+ // Polling
218
+ // ============================================================================
219
+ const POLL_INTERVAL_MS = 5_000;
220
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
221
+ async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
222
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
223
+ let consecutiveErrors = 0;
224
+ while (Date.now() < deadline) {
225
+ const result = await checkAnalysisReady(owner, repo, prNumber, token);
226
+ if (result.status === 'ready')
227
+ return 'ready';
228
+ if (result.status === 'error') {
229
+ consecutiveErrors++;
230
+ if (consecutiveErrors >= 5)
231
+ return 'error';
232
+ }
233
+ else {
234
+ consecutiveErrors = 0;
235
+ }
236
+ if (!quiet) {
237
+ process.stderr.write(`\r ${chalk.dim('Waiting for analysis to complete...')}${' '.repeat(5)}`);
238
+ }
239
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
240
+ }
241
+ return 'pending';
242
+ }
243
+ // ============================================================================
244
+ // Main command
245
+ // ============================================================================
246
+ export async function triageCommand(identifier, options) {
247
+ // Parse PR identifier
248
+ let pr;
249
+ try {
250
+ pr = parsePRIdentifier(identifier);
251
+ }
252
+ catch (err) {
253
+ console.error(chalk.red(`\n${err.message}\n`));
254
+ process.exit(1);
255
+ }
256
+ // Load GitHub token for private repo auth
257
+ const token = await loadToken() || undefined;
258
+ if (!options.json) {
259
+ console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
260
+ }
261
+ // Check if analysis is ready — resolve to a terminal state before fetching
262
+ let analysisReady = false;
263
+ const readyResult = await checkAnalysisReady(pr.owner, pr.repo, pr.prNumber, token);
264
+ if (readyResult.status === 'ready') {
265
+ analysisReady = true;
266
+ }
267
+ else if (readyResult.status === 'pending') {
268
+ if (options.wait === false) {
269
+ if (options.json) {
270
+ console.log(JSON.stringify({
271
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
272
+ }, null, 2));
273
+ }
274
+ else {
275
+ console.log(chalk.yellow('\n Analysis is still in progress.'));
276
+ console.log(chalk.dim(' Re-run without --no-wait to poll until complete.\n'));
277
+ }
278
+ return;
279
+ }
280
+ // Wait for analysis
281
+ if (!options.json) {
282
+ console.log(chalk.dim(' Analysis in progress, waiting...'));
283
+ }
284
+ const waitResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, token, options.json);
285
+ if (!options.json) {
286
+ process.stderr.write('\r' + ' '.repeat(60) + '\r');
287
+ }
288
+ if (waitResult === 'ready') {
289
+ analysisReady = true;
290
+ }
291
+ else if (waitResult === 'pending') {
292
+ if (options.json) {
293
+ console.log(JSON.stringify({
294
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
295
+ }, null, 2));
296
+ }
297
+ else {
298
+ console.log(chalk.yellow('\n Timed out waiting for analysis.'));
299
+ console.log(chalk.dim(' Try again later: ') + chalk.cyan(`haystack triage ${pr.owner}/${pr.repo}#${pr.prNumber}\n`));
300
+ }
301
+ return;
302
+ }
303
+ else {
304
+ // waitResult === 'error'
305
+ if (options.json) {
306
+ console.log(JSON.stringify({
307
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'error',
308
+ }, null, 2));
309
+ }
310
+ else {
311
+ console.error(chalk.red('\n Analysis check failed.\n'));
312
+ }
313
+ process.exit(1);
314
+ }
315
+ }
316
+ else {
317
+ // readyResult.status === 'error'
318
+ if (options.json) {
319
+ console.log(JSON.stringify({
320
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
321
+ status: 'error', message: readyResult.message,
322
+ }, null, 2));
323
+ }
324
+ else {
325
+ console.error(chalk.red(`\n Analysis check failed: ${readyResult.message}\n`));
326
+ }
327
+ process.exit(1);
328
+ }
329
+ if (!analysisReady)
330
+ return;
331
+ // Fetch the full rating synthesis
332
+ const synthesis = await fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token);
333
+ if (!synthesis) {
334
+ if (options.json) {
335
+ console.log(JSON.stringify({
336
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
337
+ status: 'error', message: 'Rating synthesis not available',
338
+ }, null, 2));
339
+ }
340
+ else {
341
+ console.error(chalk.yellow('\n Analysis completed but rating synthesis is not available yet.'));
342
+ console.log(chalk.dim(' Try again in a moment, or view the full review:'));
343
+ console.log(chalk.cyan(` https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}\n`));
344
+ }
345
+ process.exit(1);
346
+ }
347
+ // Output
348
+ if (options.json) {
349
+ printJsonOutput(pr, synthesis);
350
+ }
351
+ else {
352
+ printRichOutput(pr, synthesis);
353
+ }
354
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,9 @@
9
9
  * npx @haystackeditor/cli init # Set up .haystack.json
10
10
  * npx @haystackeditor/cli status # Check configuration
11
11
  * npx @haystackeditor/cli login # Authenticate with GitHub
12
+ * npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
13
+ * npx @haystackeditor/cli check-pending # Check analysis status
14
+ * npx @haystackeditor/cli triage 123 # View analysis results for a PR
12
15
  * npx @haystackeditor/cli secrets list # List stored secrets
13
16
  */
14
17
  export {};
package/dist/index.js CHANGED
@@ -9,6 +9,9 @@
9
9
  * npx @haystackeditor/cli init # Set up .haystack.json
10
10
  * npx @haystackeditor/cli status # Check configuration
11
11
  * npx @haystackeditor/cli login # Authenticate with GitHub
12
+ * npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
13
+ * npx @haystackeditor/cli check-pending # Check analysis status
14
+ * npx @haystackeditor/cli triage 123 # View analysis results for a PR
12
15
  * npx @haystackeditor/cli secrets list # List stored secrets
13
16
  */
14
17
  import { Command } from 'commander';
@@ -16,14 +19,19 @@ import { statusCommand } from './commands/status.js';
16
19
  import { initCommand } from './commands/init.js';
17
20
  import { loginCommand, logoutCommand } from './commands/login.js';
18
21
  import { listSecrets, setSecret, getSecret, getSecretsForRepo, deleteSecret } from './commands/secrets.js';
19
- import { handleSandbox, handleAgenticTool } from './commands/config.js';
22
+ import { handleSandbox, handleAgenticTool, handleAutoMerge, handleWaitForReviewers } from './commands/config.js';
20
23
  import { installSkills, listSkills } from './commands/skills.js';
21
24
  import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
25
+ import { submitCommand } from './commands/submit.js';
26
+ import { checkPendingCommand } from './commands/check-pending.js';
27
+ import { installSessionHooks, sessionHooksStatus } from './commands/install-session-hooks.js';
28
+ import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } from './commands/policy.js';
29
+ import { triageCommand } from './commands/triage.js';
22
30
  const program = new Command();
23
31
  program
24
32
  .name('haystack')
25
33
  .description('Set up Haystack verification for your project')
26
- .version('0.3.0');
34
+ .version('0.9.0');
27
35
  program
28
36
  .command('init')
29
37
  .description('Create .haystack.json configuration')
@@ -50,6 +58,107 @@ program
50
58
  .command('logout')
51
59
  .description('Remove stored credentials')
52
60
  .action(logoutCommand);
61
+ program
62
+ .command('submit')
63
+ .description('Create a PR from current changes')
64
+ .option('--title <title>', 'PR title (default: last commit message)')
65
+ .option('--body <body>', 'PR body (default: commit messages since base)')
66
+ .option('--body-file <path>', 'Read PR body from file (use "-" for stdin)')
67
+ .option('--base <branch>', 'Base branch (default: main or master)')
68
+ .option('--draft', 'Create as draft PR')
69
+ .option('--review [reviewer]', 'Request human review (BLOCKS auto-merge — only use when explicitly asked)')
70
+ .option('--force', 'Skip pre-PR triage checks')
71
+ .option('--no-wait', 'Skip waiting for analysis results')
72
+ .addHelpText('after', `
73
+ This command is designed for AI coding agents to submit PRs.
74
+
75
+ 1. Runs pre-PR triage (code review, rules, intent drift) via sub-agents
76
+ 2. Pushes the current branch to origin
77
+ 3. Creates a pull request on GitHub
78
+ 4. Waits for Haystack analysis results (triggered via GitHub App webhook)
79
+
80
+ Pre-PR Triage:
81
+ Before creating the PR, haystack spawns parallel sub-agents to check for:
82
+ • Code review bugs (logic errors, null crashes, security issues)
83
+ • Rule violations (from .haystack/pr-rules.yml)
84
+ • Intent drift (AI agent deviations from user instructions)
85
+
86
+ Use --force to skip triage entirely.
87
+ Use --no-wait to skip waiting for analysis results.
88
+
89
+ Review Routing:
90
+ By default, PRs enter the auto-merge queue and merge automatically once
91
+ Haystack analysis passes. This is the correct default for most PRs.
92
+
93
+ ⚠ Only use --review when the user EXPLICITLY asks for human review.
94
+ --review BLOCKS auto-merge — the PR will NOT merge until a human approves,
95
+ which delays merging. Do not add --review "just to be safe".
96
+
97
+ • --review Labels the PR "haystack:needs-review" for the
98
+ needs-assignment queue (a teammate will pick it up)
99
+ • --review <username> Same label, plus requests review from that GitHub user
100
+
101
+ Examples:
102
+ haystack submit # Triage, create PR, auto-merge queue
103
+ haystack submit --force # Skip triage
104
+ haystack submit --no-wait # Don't wait for analysis
105
+ haystack submit --title "Fix auth" # Custom PR title
106
+ haystack submit --body "## Summary" # Custom PR body (inline)
107
+ haystack submit --body-file body.md # Read body from file (supports markdown)
108
+ echo "## Summary" | haystack submit --body-file - # Read body from stdin
109
+ haystack submit --base develop # Target a different base branch
110
+ haystack submit --draft # Create as draft PR
111
+ haystack submit --review # ⚠ Blocks auto-merge, needs human approval
112
+ haystack submit --review octocat # ⚠ Blocks auto-merge, requests review from octocat
113
+ `)
114
+ .action(submitCommand);
115
+ program
116
+ .command('check-pending')
117
+ .description('Check status of pending Haystack analysis')
118
+ .option('--json', 'Output as JSON')
119
+ .option('--hook', 'Minimal output for session-start hooks')
120
+ .option('--clear', 'Clear pending submit state')
121
+ .addHelpText('after', `
122
+ After \`haystack submit\`, this checks whether the analysis finished
123
+ and shows a two-state verdict:
124
+
125
+ Good to merge — No issues found, safe to merge
126
+ Needs your input — Bugs or rule violations found
127
+
128
+ This runs automatically on CLI session start if session hooks are installed.
129
+
130
+ Examples:
131
+ haystack check-pending # Rich terminal output
132
+ haystack check-pending --hook # Minimal output (for hooks)
133
+ haystack check-pending --json # Machine-readable JSON
134
+ haystack check-pending --clear # Clear pending state
135
+ `)
136
+ .action(checkPendingCommand);
137
+ program
138
+ .command('triage <pr>')
139
+ .description('View Haystack analysis results for a PR')
140
+ .option('--json', 'Output as JSON')
141
+ .option('--no-wait', 'Exit immediately if analysis is still pending')
142
+ .addHelpText('after', `
143
+ Look up triage results (bugs, rule violations, verdict) for any PR.
144
+
145
+ PR identifier formats:
146
+ 123 PR number (uses current repo)
147
+ #123 PR number with hash
148
+ owner/repo#123 Fully qualified
149
+ https://github.com/owner/repo/pull/123 GitHub URL
150
+
151
+ By default, if analysis is still in progress, the command will poll
152
+ for up to 5 minutes. Use --no-wait to exit immediately instead.
153
+
154
+ Examples:
155
+ haystack triage 42 # Current repo, PR #42
156
+ haystack triage acme/widgets#99 # Specific repo
157
+ haystack triage https://github.com/o/r/pull/1 # From URL
158
+ haystack triage 42 --json # Machine-readable output
159
+ haystack triage 42 --no-wait # Don't wait if pending
160
+ `)
161
+ .action(triageCommand);
53
162
  // Secrets subcommands
54
163
  const secrets = program
55
164
  .command('secrets')
@@ -123,6 +232,52 @@ Examples:
123
232
  haystack config agentic-tool codex # Use your ChatGPT
124
233
  `)
125
234
  .action(handleAgenticTool);
235
+ config
236
+ .command('auto-merge [action]')
237
+ .description('Auto-merge safe PRs submitted via haystack submit (on|off|status)')
238
+ .addHelpText('after', `
239
+ Actions:
240
+ on, enable Enable auto-merge for safe PRs
241
+ off, disable Disable auto-merge
242
+ status Show current status (default)
243
+
244
+ When enabled, PRs submitted via \`haystack submit\` will be
245
+ automatically merged if Haystack analysis finds no issues
246
+ (safe to merge). PRs with bugs or rule violations still
247
+ require manual review.
248
+
249
+ Examples:
250
+ haystack config auto-merge # Show current status
251
+ haystack config auto-merge on # Enable auto-merge
252
+ haystack config auto-merge off # Disable auto-merge
253
+ `)
254
+ .action(handleAutoMerge);
255
+ config
256
+ .command('wait-for-reviewers [action] [reviewers...]')
257
+ .description('Configure which AI reviewers the merge queue waits for')
258
+ .addHelpText('after', `
259
+ Actions:
260
+ list Show current expected reviewers (default)
261
+ add <names> Add one or more AI reviewer sources
262
+ remove <names> Remove one or more AI reviewer sources
263
+ clear Remove all expected reviewers
264
+
265
+ AI reviewer sources are stored in .haystack.json under
266
+ babysitter.expected_ai_reviewers. The merge queue will
267
+ wait for these reviewers to post before starting the
268
+ grace period.
269
+
270
+ Run \`haystack config wait-for-reviewers list\` to see
271
+ all available reviewer sources and which are enabled.
272
+
273
+ Examples:
274
+ haystack config wait-for-reviewers # Show status
275
+ haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
276
+ haystack config wait-for-reviewers add cursor coderabbit # Add multiple
277
+ haystack config wait-for-reviewers remove cursor # Stop waiting for Cursor
278
+ haystack config wait-for-reviewers clear # Wait for none
279
+ `)
280
+ .action(handleWaitForReviewers);
126
281
  // Skills subcommands
127
282
  const skills = program
128
283
  .command('skills')
@@ -186,6 +341,81 @@ hooks
186
341
  .command('update')
187
342
  .description('Update Entire CLI to the latest version')
188
343
  .action(hooksUpdate);
344
+ hooks
345
+ .command('install-session')
346
+ .description('Install session-start hooks for coding CLIs')
347
+ .option('--cli <name>', 'Target CLI: claude, codex, gemini, or all')
348
+ .addHelpText('after', `
349
+ Configures your coding CLI to run \`haystack check-pending\` on session start.
350
+ This shows pending PR analysis results when you open a new terminal session.
351
+
352
+ Claude Code: Native SessionStart hook (.claude/settings.json)
353
+ Codex CLI: AGENTS.md instructions
354
+ Gemini CLI: GEMINI.md instructions
355
+
356
+ Examples:
357
+ haystack hooks install-session # Auto-detect CLIs
358
+ haystack hooks install-session --cli claude # Claude Code only
359
+ haystack hooks install-session --cli all # All detected CLIs
360
+ `)
361
+ .action(installSessionHooks);
362
+ hooks
363
+ .command('session-status')
364
+ .description('Check session hook installation status')
365
+ .action(sessionHooksStatus);
366
+ // Policy subcommands
367
+ const policy = program
368
+ .command('policy')
369
+ .description('Manage review policies (.haystack/review-policy.md)');
370
+ policy
371
+ .command('list')
372
+ .description('List all review policies')
373
+ .action(listPolicies);
374
+ policy
375
+ .command('add [name]')
376
+ .description('Add a new review policy interactively')
377
+ .addHelpText('after', `
378
+ This command prompts for:
379
+ • Policy name (e.g., "Infrastructure changes")
380
+ • File patterns (comma-separated globs, e.g., "terraform/**,*.tf")
381
+ • Severity (critical, high, medium, low)
382
+ • Reason (why this requires human review)
383
+
384
+ Examples:
385
+ haystack policy add # Interactive add
386
+ haystack policy add "Database changes" # Start with name
387
+ `)
388
+ .action(addPolicy);
389
+ policy
390
+ .command('remove <name>')
391
+ .description('Remove a review policy by name')
392
+ .action(removePolicy);
393
+ policy
394
+ .command('add-instruction [text]')
395
+ .description('Add a semantic review instruction')
396
+ .addHelpText('after', `
397
+ Instructions are natural language directives that override default review behavior.
398
+ They apply to all PRs (not file-pattern-gated).
399
+
400
+ Examples:
401
+ haystack policy add-instruction "Never flag weak test coverage as needing review"
402
+ haystack policy add-instruction # Interactive prompt
403
+ `)
404
+ .action(addInstruction);
405
+ policy
406
+ .command('init')
407
+ .description('Create initial review-policy.md with example policies')
408
+ .option('-f, --force', 'Overwrite existing file')
409
+ .addHelpText('after', `
410
+ Creates .haystack/review-policy.md with sensible defaults:
411
+ • Infrastructure changes (terraform, pulumi, cdk)
412
+ • Secret files (*.secret*, *.env*, credentials)
413
+ • CI/CD pipelines (GitHub Actions, GitLab CI, etc.)
414
+
415
+ The generated policies appear in Haystack's "Human Review Needed"
416
+ section when a PR touches matching files.
417
+ `)
418
+ .action(initPolicies);
189
419
  // Show help if no command provided
190
420
  if (process.argv.length === 2) {
191
421
  program.help();
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Auto-detection utilities for framework, monorepo, and service configuration.
3
+ * Used by the setup wizard to provide smart defaults.
4
+ */
5
+ export interface DetectedFramework {
6
+ name: string;
7
+ devCommand: string;
8
+ port: number;
9
+ readyPattern: string;
10
+ configFile: string;
11
+ }
12
+ export interface DetectedService {
13
+ name: string;
14
+ root: string;
15
+ type: 'server' | 'batch';
16
+ framework?: DetectedFramework;
17
+ /** Services this service depends on (detected from proxy config) */
18
+ depends_on?: string[];
19
+ }
20
+ export interface ProxyTarget {
21
+ /** Path pattern being proxied (e.g., "/api") */
22
+ path: string;
23
+ /** Target URL (e.g., "http://localhost:8787") */
24
+ target: string;
25
+ /** Port extracted from target */
26
+ port: number;
27
+ }
28
+ export interface ProjectDetection {
29
+ isMonorepo: boolean;
30
+ packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun';
31
+ frameworks: DetectedFramework[];
32
+ services: DetectedService[];
33
+ /** Proxy targets detected from config files */
34
+ proxyTargets: ProxyTarget[];
35
+ suggestions: {
36
+ projectName: string;
37
+ devCommand: string;
38
+ port: number;
39
+ readyPattern: string;
40
+ authBypassEnv?: string;
41
+ };
42
+ }
43
+ /**
44
+ * Main detection function - analyzes a project directory
45
+ */
46
+ export declare function detectProject(rootDir?: string): ProjectDetection;
47
+ /**
48
+ * Format detection results for display to user
49
+ */
50
+ export declare function formatDetectionSummary(detection: ProjectDetection): string;