@haystackeditor/cli 0.8.0 → 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 (76) hide show
  1. package/README.md +105 -12
  2. package/dist/assets/hooks/agent-context/detect.ts +136 -0
  3. package/dist/assets/hooks/agent-context/format.ts +99 -0
  4. package/dist/assets/hooks/agent-context/index.ts +39 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
  6. package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
  7. package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
  8. package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
  9. package/dist/assets/hooks/agent-context/types.ts +58 -0
  10. package/dist/assets/hooks/llm-rules-template.md +56 -0
  11. package/dist/assets/hooks/package.json +11 -0
  12. package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
  13. package/dist/assets/hooks/scripts/post-commit.sh +4 -0
  14. package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
  15. package/dist/assets/hooks/scripts/pre-push.sh +25 -0
  16. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  17. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
  18. package/dist/assets/hooks/truncation-checker/index.ts +595 -0
  19. package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
  20. package/dist/assets/skills/prepare-haystack.md +323 -0
  21. package/dist/assets/skills/secrets.md +164 -0
  22. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  23. package/dist/assets/skills/setup-haystack.md +639 -0
  24. package/dist/assets/skills/submit.md +154 -0
  25. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  26. package/dist/assets/templates/haystack.yml +193 -0
  27. package/dist/commands/check-pending.d.ts +19 -0
  28. package/dist/commands/check-pending.js +217 -0
  29. package/dist/commands/config.d.ts +18 -12
  30. package/dist/commands/config.js +327 -52
  31. package/dist/commands/hooks.d.ts +17 -0
  32. package/dist/commands/hooks.js +269 -0
  33. package/dist/commands/install-session-hooks.d.ts +16 -0
  34. package/dist/commands/install-session-hooks.js +302 -0
  35. package/dist/commands/login.js +1 -1
  36. package/dist/commands/policy.d.ts +31 -0
  37. package/dist/commands/policy.js +365 -0
  38. package/dist/commands/skills.d.ts +8 -0
  39. package/dist/commands/skills.js +80 -0
  40. package/dist/commands/submit.d.ts +22 -0
  41. package/dist/commands/submit.js +428 -0
  42. package/dist/commands/triage.d.ts +16 -0
  43. package/dist/commands/triage.js +354 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +317 -2
  46. package/dist/tools/detect.d.ts +50 -0
  47. package/dist/tools/detect.js +853 -0
  48. package/dist/tools/fixtures.d.ts +38 -0
  49. package/dist/tools/fixtures.js +199 -0
  50. package/dist/tools/setup.d.ts +43 -0
  51. package/dist/tools/setup.js +597 -0
  52. package/dist/triage/prompts.d.ts +31 -0
  53. package/dist/triage/prompts.js +266 -0
  54. package/dist/triage/runner.d.ts +21 -0
  55. package/dist/triage/runner.js +325 -0
  56. package/dist/triage/traces.d.ts +20 -0
  57. package/dist/triage/traces.js +305 -0
  58. package/dist/triage/types.d.ts +47 -0
  59. package/dist/triage/types.js +7 -0
  60. package/dist/types.d.ts +1387 -191
  61. package/dist/types.js +254 -2
  62. package/dist/utils/analysis-api.d.ts +108 -0
  63. package/dist/utils/analysis-api.js +194 -0
  64. package/dist/utils/config.js +1 -1
  65. package/dist/utils/git.d.ts +80 -0
  66. package/dist/utils/git.js +302 -0
  67. package/dist/utils/github-api.d.ts +83 -0
  68. package/dist/utils/github-api.js +266 -0
  69. package/dist/utils/hooks.d.ts +26 -0
  70. package/dist/utils/hooks.js +226 -0
  71. package/dist/utils/pending-state.d.ts +38 -0
  72. package/dist/utils/pending-state.js +86 -0
  73. package/dist/utils/secrets.js +3 -3
  74. package/dist/utils/skill.d.ts +1 -1
  75. package/dist/utils/skill.js +658 -1
  76. package/package.json +5 -3
@@ -1,9 +1,69 @@
1
1
  /**
2
- * Config commands - manage user preferences stored on Haystack Platform
2
+ * Config commands - manage preferences
3
+ *
4
+ * Repo-level preferences (sandbox_enabled, auto_merge) are stored in .haystack.json.
5
+ * User-level preferences (agentic_tool) are stored on the Haystack Platform API.
3
6
  */
4
7
  import chalk from 'chalk';
8
+ import { readFileSync, writeFileSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ import { execSync } from 'child_process';
5
11
  import { loadToken } from './login.js';
12
+ import { AI_REVIEWER_SOURCES, AI_REVIEWER_DISPLAY_NAMES } from '../types.js';
6
13
  const API_BASE = 'https://haystackeditor.com/api/preferences';
14
+ const AGENTIC_TOOL_LABELS = {
15
+ 'opencode': 'OpenCode (Haystack billing)',
16
+ 'claude-code': 'Claude Code (your Claude Max subscription)',
17
+ 'codex': 'Codex CLI (your ChatGPT subscription)',
18
+ };
19
+ // ============================================================================
20
+ // .haystack.json helpers (repo-level preferences)
21
+ // ============================================================================
22
+ function findRepoRoot() {
23
+ try {
24
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
25
+ }
26
+ catch {
27
+ console.error(chalk.red('\nNot inside a git repository.\n'));
28
+ process.exit(1);
29
+ }
30
+ }
31
+ function loadHaystackConfig() {
32
+ const root = findRepoRoot();
33
+ const configPath = resolve(root, '.haystack.json');
34
+ try {
35
+ const raw = readFileSync(configPath, 'utf-8');
36
+ return { config: JSON.parse(raw), path: configPath };
37
+ }
38
+ catch (err) {
39
+ if (err.code === 'ENOENT') {
40
+ console.error(chalk.red(`\nNo .haystack.json found at ${configPath}`));
41
+ console.log(chalk.dim('Create one with: haystack init\n'));
42
+ process.exit(1);
43
+ }
44
+ throw err;
45
+ }
46
+ }
47
+ function saveHaystackConfig(config, configPath) {
48
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
49
+ }
50
+ function getPreference(key) {
51
+ const { config } = loadHaystackConfig();
52
+ // Default: sandbox_enabled=true, auto_merge=false
53
+ const defaults = { sandbox_enabled: true, auto_merge: false };
54
+ return config.preferences?.[key] ?? defaults[key];
55
+ }
56
+ function setPreference(key, value) {
57
+ const { config, path } = loadHaystackConfig();
58
+ if (!config.preferences) {
59
+ config.preferences = {};
60
+ }
61
+ config.preferences[key] = value;
62
+ saveHaystackConfig(config, path);
63
+ }
64
+ // ============================================================================
65
+ // API helpers (user-level preferences)
66
+ // ============================================================================
7
67
  async function requireAuth() {
8
68
  const token = await loadToken();
9
69
  if (!token) {
@@ -13,7 +73,7 @@ async function requireAuth() {
13
73
  return token;
14
74
  }
15
75
  async function apiRequest(method, token, body) {
16
- const response = await fetch(API_BASE, {
76
+ return fetch(API_BASE, {
17
77
  method,
18
78
  headers: {
19
79
  'Authorization': `Bearer ${token}`,
@@ -22,12 +82,64 @@ async function apiRequest(method, token, body) {
22
82
  },
23
83
  body: body ? JSON.stringify(body) : undefined,
24
84
  });
25
- return response;
26
85
  }
27
- /**
28
- * Get current sandbox status
29
- */
86
+ // ============================================================================
87
+ // Sandbox (repo-level via .haystack.json)
88
+ // ============================================================================
30
89
  export async function getSandboxStatus() {
90
+ const enabled = getPreference('sandbox_enabled');
91
+ console.log(chalk.bold('\nSandbox mode:'), enabled
92
+ ? chalk.green('enabled')
93
+ : chalk.yellow('disabled'));
94
+ console.log();
95
+ if (enabled) {
96
+ console.log(chalk.dim('Haystack can clone and store your repos for verification.'));
97
+ console.log(chalk.dim('All data is deleted after 24 hours.'));
98
+ }
99
+ else {
100
+ console.log(chalk.dim('Sandbox mode is disabled. Automated verification will not work.'));
101
+ console.log(chalk.dim('Enable with: haystack config sandbox on'));
102
+ }
103
+ console.log(chalk.dim('(Stored in .haystack.json — applies to all repo contributors)'));
104
+ console.log();
105
+ }
106
+ export async function enableSandbox() {
107
+ setPreference('sandbox_enabled', true);
108
+ console.log(chalk.green('\nSandbox mode enabled.\n'));
109
+ console.log(chalk.dim('Haystack can now clone and store your repos for verification.'));
110
+ console.log(chalk.dim('All data is deleted after 24 hours.'));
111
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
112
+ }
113
+ export async function disableSandbox() {
114
+ setPreference('sandbox_enabled', false);
115
+ console.log(chalk.yellow('\nSandbox mode disabled.\n'));
116
+ console.log(chalk.dim('Automated verification will not work until you re-enable sandbox mode.'));
117
+ console.log(chalk.dim('Re-enable with: haystack config sandbox on'));
118
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
119
+ }
120
+ export async function handleSandbox(action) {
121
+ switch (action?.toLowerCase()) {
122
+ case 'on':
123
+ case 'enable':
124
+ case 'true':
125
+ return enableSandbox();
126
+ case 'off':
127
+ case 'disable':
128
+ case 'false':
129
+ return disableSandbox();
130
+ case 'status':
131
+ case undefined:
132
+ return getSandboxStatus();
133
+ default:
134
+ console.error(chalk.red(`\nUnknown action: ${action}`));
135
+ console.log('\nUsage: haystack config sandbox [on|off|status]\n');
136
+ process.exit(1);
137
+ }
138
+ }
139
+ // ============================================================================
140
+ // Agentic tool (user-level via API)
141
+ // ============================================================================
142
+ export async function getAgenticToolStatus() {
31
143
  const token = await requireAuth();
32
144
  console.log(chalk.dim('\nFetching preferences...\n'));
33
145
  try {
@@ -40,33 +152,28 @@ export async function getSandboxStatus() {
40
152
  throw new Error(`Failed to get preferences: ${response.status}`);
41
153
  }
42
154
  const data = await response.json();
43
- console.log(chalk.bold('Sandbox mode:'), data.sandbox_enabled
44
- ? chalk.green('enabled')
45
- : chalk.yellow('disabled'));
155
+ const currentTool = data.agentic_tool || 'opencode';
156
+ console.log(chalk.bold('Agentic tool:'), chalk.cyan(AGENTIC_TOOL_LABELS[currentTool]));
46
157
  console.log();
47
- if (data.sandbox_enabled) {
48
- console.log(chalk.dim('Haystack can clone and store your repos for verification.'));
49
- console.log(chalk.dim('All data is deleted after 24 hours.'));
50
- }
51
- else {
52
- console.log(chalk.dim('Sandbox mode is disabled. Automated verification will not work.'));
53
- console.log(chalk.dim('Enable with: haystack config sandbox on'));
158
+ console.log(chalk.dim('Available options:'));
159
+ for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
160
+ const marker = key === currentTool ? chalk.green(' ✓ ') : ' ';
161
+ console.log(`${marker}${chalk.dim(key)} - ${label}`);
54
162
  }
55
163
  console.log();
164
+ console.log(chalk.dim('Change with: haystack config agentic-tool <tool>'));
165
+ console.log();
56
166
  }
57
167
  catch (error) {
58
168
  console.error(chalk.red(`\nError: ${error.message}\n`));
59
169
  process.exit(1);
60
170
  }
61
171
  }
62
- /**
63
- * Enable sandbox mode
64
- */
65
- export async function enableSandbox() {
172
+ export async function setAgenticTool(tool) {
66
173
  const token = await requireAuth();
67
- console.log(chalk.dim('\nEnabling sandbox mode...'));
174
+ console.log(chalk.dim(`\nSetting agentic tool to ${tool}...`));
68
175
  try {
69
- const response = await apiRequest('PUT', token, { sandbox_enabled: true });
176
+ const response = await apiRequest('PUT', token, { agentic_tool: tool });
70
177
  if (!response.ok) {
71
178
  if (response.status === 401) {
72
179
  console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
@@ -75,59 +182,227 @@ export async function enableSandbox() {
75
182
  const error = await response.json().catch(() => ({}));
76
183
  throw new Error(error.error || `Failed to update preferences: ${response.status}`);
77
184
  }
78
- console.log(chalk.green('\nSandbox mode enabled.\n'));
79
- console.log(chalk.dim('Haystack can now clone and store your repos for verification.'));
80
- console.log(chalk.dim('All data is deleted after 24 hours.\n'));
185
+ console.log(chalk.green(`\nAgentic tool set to: ${AGENTIC_TOOL_LABELS[tool]}\n`));
186
+ if (tool === 'claude-code') {
187
+ console.log(chalk.dim('Note: Requires Claude Max subscription.'));
188
+ console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
189
+ }
190
+ else if (tool === 'codex') {
191
+ console.log(chalk.dim('Note: Requires ChatGPT Plus, Pro, or Team subscription.'));
192
+ console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
193
+ }
81
194
  }
82
195
  catch (error) {
83
196
  console.error(chalk.red(`\nError: ${error.message}\n`));
84
197
  process.exit(1);
85
198
  }
86
199
  }
87
- /**
88
- * Disable sandbox mode
89
- */
90
- export async function disableSandbox() {
91
- const token = await requireAuth();
92
- console.log(chalk.dim('\nDisabling sandbox mode...'));
93
- try {
94
- const response = await apiRequest('PUT', token, { sandbox_enabled: false });
95
- if (!response.ok) {
96
- if (response.status === 401) {
97
- console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
98
- process.exit(1);
99
- }
100
- const error = await response.json().catch(() => ({}));
101
- throw new Error(error.error || `Failed to update preferences: ${response.status}`);
102
- }
103
- console.log(chalk.yellow('\nSandbox mode disabled.\n'));
104
- console.log(chalk.dim('Automated verification will not work until you re-enable sandbox mode.'));
105
- console.log(chalk.dim('Re-enable with: haystack config sandbox on\n'));
200
+ export async function handleAgenticTool(tool) {
201
+ const validTools = ['opencode', 'claude-code', 'codex'];
202
+ if (!tool || tool === 'status') {
203
+ return getAgenticToolStatus();
106
204
  }
107
- catch (error) {
108
- console.error(chalk.red(`\nError: ${error.message}\n`));
205
+ const normalizedTool = tool.toLowerCase();
206
+ if (!validTools.includes(normalizedTool)) {
207
+ console.error(chalk.red(`\nUnknown tool: ${tool}`));
208
+ console.log('\nValid options:');
209
+ for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
210
+ console.log(` ${chalk.cyan(key)} - ${label}`);
211
+ }
212
+ console.log('\nUsage: haystack config agentic-tool [opencode|claude-code|codex|status]\n');
109
213
  process.exit(1);
110
214
  }
215
+ return setAgenticTool(normalizedTool);
111
216
  }
217
+ // ============================================================================
218
+ // Auto-merge (repo-level via .haystack.json)
219
+ // ============================================================================
112
220
  /**
113
- * Handle sandbox subcommand
221
+ * Check if auto-merge is enabled for the current repo.
222
+ * Returns false if .haystack.json is missing or on error.
114
223
  */
115
- export async function handleSandbox(action) {
224
+ export async function isAutoMergeEnabled() {
225
+ try {
226
+ return getPreference('auto_merge');
227
+ }
228
+ catch {
229
+ return false;
230
+ }
231
+ }
232
+ export async function getAutoMergeStatus() {
233
+ const enabled = getPreference('auto_merge');
234
+ console.log(chalk.bold('\nAuto-merge:'), enabled
235
+ ? chalk.green('enabled')
236
+ : chalk.yellow('disabled'));
237
+ console.log();
238
+ if (enabled) {
239
+ console.log(chalk.dim('PRs submitted via `haystack submit` will be auto-merged'));
240
+ console.log(chalk.dim('when analysis finds no issues (safe to merge).'));
241
+ }
242
+ else {
243
+ console.log(chalk.dim('Auto-merge is disabled. PRs require manual merge.'));
244
+ console.log(chalk.dim('Enable with: haystack config auto-merge on'));
245
+ }
246
+ console.log(chalk.dim('(Stored in .haystack.json — applies to all repo contributors)'));
247
+ console.log();
248
+ }
249
+ export async function enableAutoMerge() {
250
+ setPreference('auto_merge', true);
251
+ console.log(chalk.green('\nAuto-merge enabled.\n'));
252
+ console.log(chalk.dim('PRs submitted via `haystack submit` will be auto-merged'));
253
+ console.log(chalk.dim('when analysis finds no issues (safe to merge).'));
254
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
255
+ }
256
+ export async function disableAutoMerge() {
257
+ setPreference('auto_merge', false);
258
+ console.log(chalk.yellow('\nAuto-merge disabled.\n'));
259
+ console.log(chalk.dim('PRs will require manual merge after analysis.'));
260
+ console.log(chalk.dim('Re-enable with: haystack config auto-merge on'));
261
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
262
+ }
263
+ export async function handleAutoMerge(action) {
116
264
  switch (action?.toLowerCase()) {
117
265
  case 'on':
118
266
  case 'enable':
119
267
  case 'true':
120
- return enableSandbox();
268
+ return enableAutoMerge();
121
269
  case 'off':
122
270
  case 'disable':
123
271
  case 'false':
124
- return disableSandbox();
272
+ return disableAutoMerge();
125
273
  case 'status':
126
274
  case undefined:
127
- return getSandboxStatus();
275
+ return getAutoMergeStatus();
128
276
  default:
129
277
  console.error(chalk.red(`\nUnknown action: ${action}`));
130
- console.log('\nUsage: haystack config sandbox [on|off|status]\n');
278
+ console.log('\nUsage: haystack config auto-merge [on|off|status]\n');
131
279
  process.exit(1);
132
280
  }
133
281
  }
282
+ // ============================================================================
283
+ // Wait-for-reviewers (repo-level via .haystack.json)
284
+ // ============================================================================
285
+ function getBabysitterConfig() {
286
+ const { config, path } = loadHaystackConfig();
287
+ const babysitter = config.babysitter ?? {};
288
+ const reviewers = babysitter.expected_ai_reviewers ?? [];
289
+ return { config, path, reviewers };
290
+ }
291
+ function saveBabysitterReviewers(config, configPath, reviewers) {
292
+ const babysitter = config.babysitter ?? {};
293
+ babysitter.expected_ai_reviewers = reviewers.length > 0 ? reviewers : undefined;
294
+ config.babysitter = babysitter;
295
+ saveHaystackConfig(config, configPath);
296
+ }
297
+ function printAvailableReviewers(currentReviewers) {
298
+ const currentSet = new Set(currentReviewers);
299
+ console.log(chalk.dim('Available AI reviewer sources:'));
300
+ for (const source of AI_REVIEWER_SOURCES) {
301
+ const name = AI_REVIEWER_DISPLAY_NAMES[source];
302
+ const active = currentSet.has(source);
303
+ const marker = active ? chalk.green(' ✓ ') : ' ';
304
+ console.log(`${marker}${chalk.cyan(source)} — ${name}`);
305
+ }
306
+ console.log();
307
+ }
308
+ function validateReviewerSources(sources) {
309
+ const validSet = new Set(AI_REVIEWER_SOURCES);
310
+ const invalid = sources.filter(s => !validSet.has(s));
311
+ if (invalid.length > 0) {
312
+ console.error(chalk.red(`\nUnknown reviewer source(s): ${invalid.join(', ')}`));
313
+ console.log(chalk.dim(`\nValid sources: ${AI_REVIEWER_SOURCES.join(', ')}`));
314
+ process.exit(1);
315
+ }
316
+ return sources;
317
+ }
318
+ export async function handleWaitForReviewers(action, reviewers) {
319
+ const normalizedAction = action?.toLowerCase();
320
+ // Default: show status
321
+ if (!normalizedAction || normalizedAction === 'list' || normalizedAction === 'status') {
322
+ const { reviewers: current } = getBabysitterConfig();
323
+ console.log(chalk.bold('\nMerge queue — expected AI reviewers\n'));
324
+ if (current.length > 0) {
325
+ console.log('The merge queue will wait for these external reviewers before merging:');
326
+ for (const source of current) {
327
+ const name = AI_REVIEWER_DISPLAY_NAMES[source] ?? source;
328
+ console.log(` ${chalk.green(name)} ${chalk.dim(`(${source})`)}`);
329
+ }
330
+ }
331
+ else {
332
+ console.log('No external AI reviewers configured.');
333
+ console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
334
+ }
335
+ console.log();
336
+ printAvailableReviewers(current);
337
+ console.log(chalk.dim('Add with: haystack config wait-for-reviewers add <source>'));
338
+ console.log();
339
+ return;
340
+ }
341
+ if (normalizedAction === 'add') {
342
+ if (!reviewers || reviewers.length === 0) {
343
+ console.error(chalk.red('\nSpecify at least one reviewer source to add.'));
344
+ const { reviewers: current } = getBabysitterConfig();
345
+ console.log();
346
+ printAvailableReviewers(current);
347
+ process.exit(1);
348
+ }
349
+ const validated = validateReviewerSources(reviewers);
350
+ const { config, path, reviewers: current } = getBabysitterConfig();
351
+ const merged = Array.from(new Set([...current, ...validated]));
352
+ saveBabysitterReviewers(config, path, merged);
353
+ const added = validated.filter(s => !current.includes(s));
354
+ if (added.length > 0) {
355
+ console.log(chalk.green(`\nAdded: ${added.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
356
+ }
357
+ else {
358
+ console.log(chalk.dim('\nAll specified reviewers were already configured.'));
359
+ }
360
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
361
+ return;
362
+ }
363
+ if (normalizedAction === 'remove') {
364
+ if (!reviewers || reviewers.length === 0) {
365
+ console.error(chalk.red('\nSpecify at least one reviewer source to remove.'));
366
+ process.exit(1);
367
+ }
368
+ const validated = validateReviewerSources(reviewers);
369
+ const { config, path, reviewers: current } = getBabysitterConfig();
370
+ const removeSet = new Set(validated);
371
+ const remaining = current.filter(s => !removeSet.has(s));
372
+ saveBabysitterReviewers(config, path, remaining);
373
+ const removed = validated.filter(s => current.includes(s));
374
+ if (removed.length > 0) {
375
+ console.log(chalk.yellow(`\nRemoved: ${removed.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
376
+ }
377
+ else {
378
+ console.log(chalk.dim('\nNone of the specified reviewers were configured.'));
379
+ }
380
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
381
+ return;
382
+ }
383
+ if (normalizedAction === 'clear') {
384
+ const { config, path } = getBabysitterConfig();
385
+ saveBabysitterReviewers(config, path, []);
386
+ console.log(chalk.yellow('\nCleared all expected AI reviewers.'));
387
+ console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
388
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
389
+ return;
390
+ }
391
+ // Unknown action — maybe the user passed a reviewer name directly
392
+ // Try treating action + reviewers as all reviewer names for 'add'
393
+ const allNames = [action, ...(reviewers ?? [])];
394
+ const validSet = new Set(AI_REVIEWER_SOURCES);
395
+ if (allNames.every(n => validSet.has(n))) {
396
+ // Treat as implicit 'add'
397
+ const validated = validateReviewerSources(allNames);
398
+ const { config, path, reviewers: current } = getBabysitterConfig();
399
+ const merged = Array.from(new Set([...current, ...validated]));
400
+ saveBabysitterReviewers(config, path, merged);
401
+ console.log(chalk.green(`\nAdded: ${validated.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
402
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
403
+ return;
404
+ }
405
+ console.error(chalk.red(`\nUnknown action: ${action}`));
406
+ console.log('\nUsage: haystack config wait-for-reviewers [list|add|remove|clear] [sources...]\n');
407
+ process.exit(1);
408
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * haystack hooks - Install and manage git hooks for AI agent quality checks
3
+ *
4
+ * Installs:
5
+ * - Entire CLI binary (session tracking, powered by https://entire.dev)
6
+ * - Pre-commit hook (agent detection, truncation checking, LLM rules review)
7
+ * - Git hooks for session logging (commit-msg, post-commit, pre-push, prepare-commit-msg)
8
+ */
9
+ interface HooksInstallOptions {
10
+ version?: string;
11
+ force?: boolean;
12
+ skipEntire?: boolean;
13
+ }
14
+ export declare function hooksInstall(options: HooksInstallOptions): Promise<void>;
15
+ export declare function hooksStatus(): Promise<void>;
16
+ export declare function hooksUpdate(): Promise<void>;
17
+ export {};