@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
@@ -0,0 +1,269 @@
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
+ import chalk from 'chalk';
10
+ import { existsSync, accessSync, constants } from 'fs';
11
+ import * as path from 'path';
12
+ import { execSync } from 'child_process';
13
+ import { findGitRoot, ENTIRE_DEFAULT_VERSION, ENTIRE_BIN_PATH, getInstalledEntireVersion, getLatestEntireVersion, downloadEntireBinary, copyHookFiles, installHookDeps, copyLlmRulesTemplate, createEntireConfig, updateGitignore, } from '../utils/hooks.js';
14
+ export async function hooksInstall(options) {
15
+ console.log(chalk.cyan('\nHaystack Hooks Setup\n'));
16
+ // Step 1: Validate git repo
17
+ const gitRoot = findGitRoot();
18
+ if (!gitRoot) {
19
+ console.error(chalk.red('Not a git repository. Run this from inside a git repo.\n'));
20
+ process.exit(1);
21
+ }
22
+ const hooksDir = path.join(gitRoot, 'hooks');
23
+ // Step 2: Check for existing installation
24
+ if (existsSync(hooksDir) && !options.force) {
25
+ console.error(chalk.yellow('hooks/ directory already exists. Use --force to overwrite.\n'));
26
+ process.exit(1);
27
+ }
28
+ if (existsSync(hooksDir) && options.force) {
29
+ console.log(chalk.yellow('Overwriting existing hooks...'));
30
+ }
31
+ // Step 3: Download Entire binary
32
+ if (!options.skipEntire) {
33
+ const version = options.version || ENTIRE_DEFAULT_VERSION;
34
+ const installedVersion = await getInstalledEntireVersion();
35
+ if (installedVersion === version) {
36
+ console.log(chalk.green(`✓ Entire CLI v${version} already installed`));
37
+ }
38
+ else {
39
+ console.log(chalk.dim(`Downloading Entire CLI v${version}...`));
40
+ try {
41
+ await downloadEntireBinary(version);
42
+ console.log(chalk.green(`✓ Entire CLI v${version} installed to ~/.haystack/bin/entire`));
43
+ }
44
+ catch (err) {
45
+ const message = err instanceof Error ? err.message : String(err);
46
+ console.error(chalk.red(`Failed to download Entire CLI: ${message}\n`));
47
+ process.exit(1);
48
+ }
49
+ }
50
+ }
51
+ else {
52
+ console.log(chalk.dim('Skipping Entire CLI download (--skip-entire)'));
53
+ }
54
+ // Step 4: Copy hook files
55
+ try {
56
+ await copyHookFiles(hooksDir);
57
+ console.log(chalk.green('✓ Hook scripts installed to hooks/'));
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ console.error(chalk.red(`Failed to copy hook files: ${message}\n`));
62
+ process.exit(1);
63
+ }
64
+ // Step 5: Install hook dependencies
65
+ console.log(chalk.dim('Installing hook dependencies (tree-sitter)...'));
66
+ try {
67
+ await installHookDeps(hooksDir);
68
+ console.log(chalk.green('✓ Hook dependencies installed'));
69
+ }
70
+ catch {
71
+ console.log(chalk.yellow('⚠ Failed to install hook dependencies. Run `npm install` in hooks/ manually.'));
72
+ }
73
+ // Step 6: Configure git hooks path
74
+ try {
75
+ execSync('git config core.hooksPath hooks', { cwd: gitRoot, stdio: 'pipe' });
76
+ console.log(chalk.green('✓ Git configured to use hooks/ directory'));
77
+ }
78
+ catch (err) {
79
+ const message = err instanceof Error ? err.message : String(err);
80
+ console.error(chalk.red(`Failed to set git hooks path: ${message}\n`));
81
+ process.exit(1);
82
+ }
83
+ // Step 7: Create .entire/ configuration
84
+ try {
85
+ await createEntireConfig(gitRoot);
86
+ console.log(chalk.green('✓ Created .entire/settings.json'));
87
+ }
88
+ catch {
89
+ console.log(chalk.yellow('⚠ Failed to create .entire/ configuration'));
90
+ }
91
+ // Step 8: Create LLM_RULES.md if needed
92
+ try {
93
+ const created = await copyLlmRulesTemplate(gitRoot);
94
+ if (created) {
95
+ console.log(chalk.green('✓ Created LLM_RULES.md (customize this for your project)'));
96
+ }
97
+ else {
98
+ console.log(chalk.dim(' LLM_RULES.md already exists'));
99
+ }
100
+ }
101
+ catch {
102
+ console.log(chalk.yellow('⚠ Failed to create LLM_RULES.md'));
103
+ }
104
+ // Step 9: Update .gitignore
105
+ try {
106
+ const updated = await updateGitignore(gitRoot);
107
+ if (updated) {
108
+ console.log(chalk.green('✓ Updated .gitignore'));
109
+ }
110
+ }
111
+ catch {
112
+ console.log(chalk.yellow('⚠ Failed to update .gitignore'));
113
+ }
114
+ // Summary
115
+ console.log(chalk.green('\n✓ Haystack hooks installed!\n'));
116
+ console.log(chalk.dim('Hooks installed:'));
117
+ console.log(chalk.dim(' pre-commit Agent detection, truncation check, LLM rules review'));
118
+ console.log(chalk.dim(' commit-msg Session tracking (powered by Entire)'));
119
+ console.log(chalk.dim(' post-commit Session condensing (powered by Entire)'));
120
+ console.log(chalk.dim(' pre-push Session log push (powered by Entire)'));
121
+ console.log(chalk.dim(' prepare-commit-msg Commit preparation (powered by Entire)'));
122
+ console.log('');
123
+ console.log(chalk.dim('Files to commit:'));
124
+ console.log(chalk.dim(' hooks/ Hook scripts and TypeScript modules'));
125
+ console.log(chalk.dim(' .entire/settings.json'));
126
+ console.log(chalk.dim(' .entire/.gitignore'));
127
+ console.log(chalk.dim(' LLM_RULES.md Customize this for your project'));
128
+ console.log('');
129
+ }
130
+ // ============================================================================
131
+ // haystack hooks status
132
+ // ============================================================================
133
+ export async function hooksStatus() {
134
+ const gitRoot = findGitRoot();
135
+ if (!gitRoot) {
136
+ console.error(chalk.red('Not a git repository.\n'));
137
+ process.exit(1);
138
+ }
139
+ console.log(chalk.cyan('\nHaystack Hooks Status\n'));
140
+ const hooksDir = path.join(gitRoot, 'hooks');
141
+ // Check hooks directory
142
+ if (!existsSync(hooksDir)) {
143
+ console.log(chalk.red('✗ hooks/ directory not found'));
144
+ console.log(chalk.dim(' Run: haystack hooks install\n'));
145
+ return;
146
+ }
147
+ console.log(chalk.green('✓ hooks/ directory exists'));
148
+ // Check each hook script
149
+ const requiredHooks = [
150
+ 'pre-commit',
151
+ 'commit-msg',
152
+ 'post-commit',
153
+ 'pre-push',
154
+ 'prepare-commit-msg',
155
+ ];
156
+ for (const hook of requiredHooks) {
157
+ const hookPath = path.join(hooksDir, hook);
158
+ if (existsSync(hookPath)) {
159
+ try {
160
+ accessSync(hookPath, constants.X_OK);
161
+ console.log(chalk.green(`✓ ${hook}`));
162
+ }
163
+ catch {
164
+ console.log(chalk.yellow(`⚠ ${hook} (not executable)`));
165
+ }
166
+ }
167
+ else {
168
+ console.log(chalk.red(`✗ ${hook} missing`));
169
+ }
170
+ }
171
+ // Check TypeScript modules
172
+ const agentContextDir = path.join(hooksDir, 'agent-context');
173
+ const truncationDir = path.join(hooksDir, 'truncation-checker');
174
+ console.log(existsSync(agentContextDir)
175
+ ? chalk.green('✓ agent-context/ module')
176
+ : chalk.red('✗ agent-context/ missing'));
177
+ console.log(existsSync(truncationDir)
178
+ ? chalk.green('✓ truncation-checker/ module')
179
+ : chalk.red('✗ truncation-checker/ missing'));
180
+ // Check hook dependencies
181
+ const nodeModules = path.join(hooksDir, 'node_modules');
182
+ console.log(existsSync(nodeModules)
183
+ ? chalk.green('✓ Hook dependencies installed')
184
+ : chalk.yellow('⚠ Hook dependencies not installed (run npm install in hooks/)'));
185
+ // Check git core.hooksPath
186
+ try {
187
+ const hooksPath = execSync('git config core.hooksPath', {
188
+ cwd: gitRoot,
189
+ encoding: 'utf-8',
190
+ stdio: ['pipe', 'pipe', 'pipe'],
191
+ }).trim();
192
+ if (hooksPath === 'hooks') {
193
+ console.log(chalk.green('✓ Git core.hooksPath = hooks'));
194
+ }
195
+ else {
196
+ console.log(chalk.yellow(`⚠ Git core.hooksPath = ${hooksPath} (expected: hooks)`));
197
+ }
198
+ }
199
+ catch {
200
+ console.log(chalk.red('✗ Git core.hooksPath not set'));
201
+ }
202
+ // Check Entire binary
203
+ const installedVersion = await getInstalledEntireVersion();
204
+ if (installedVersion) {
205
+ console.log(chalk.green(`✓ Entire CLI v${installedVersion}`));
206
+ }
207
+ else if (existsSync(ENTIRE_BIN_PATH)) {
208
+ console.log(chalk.yellow('⚠ Entire CLI binary exists but failed to run'));
209
+ }
210
+ else {
211
+ console.log(chalk.yellow('⚠ Entire CLI not installed (~/.haystack/bin/entire)'));
212
+ }
213
+ // Check .entire/settings.json
214
+ const settingsPath = path.join(gitRoot, '.entire', 'settings.json');
215
+ if (existsSync(settingsPath)) {
216
+ console.log(chalk.green('✓ .entire/settings.json'));
217
+ }
218
+ else {
219
+ console.log(chalk.yellow('⚠ .entire/settings.json not found'));
220
+ }
221
+ // Check LLM_RULES.md
222
+ const rulesPath = path.join(gitRoot, 'LLM_RULES.md');
223
+ if (existsSync(rulesPath)) {
224
+ console.log(chalk.green('✓ LLM_RULES.md'));
225
+ }
226
+ else {
227
+ console.log(chalk.yellow('⚠ LLM_RULES.md not found'));
228
+ }
229
+ console.log('');
230
+ }
231
+ // ============================================================================
232
+ // haystack hooks update
233
+ // ============================================================================
234
+ export async function hooksUpdate() {
235
+ console.log(chalk.cyan('\nUpdating Entire CLI...\n'));
236
+ // Check current version
237
+ const currentVersion = await getInstalledEntireVersion();
238
+ if (currentVersion) {
239
+ console.log(chalk.dim(`Current version: v${currentVersion}`));
240
+ }
241
+ else {
242
+ console.log(chalk.dim('Entire CLI not currently installed.'));
243
+ }
244
+ // Fetch latest version
245
+ let latestVersion;
246
+ try {
247
+ latestVersion = await getLatestEntireVersion();
248
+ }
249
+ catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ console.error(chalk.red(`Failed to check for updates: ${message}\n`));
252
+ process.exit(1);
253
+ }
254
+ console.log(chalk.dim(`Latest version: v${latestVersion}`));
255
+ if (currentVersion === latestVersion) {
256
+ console.log(chalk.green('\n✓ Already up to date.\n'));
257
+ return;
258
+ }
259
+ // Download and install
260
+ try {
261
+ await downloadEntireBinary(latestVersion);
262
+ console.log(chalk.green(`\n✓ Updated Entire CLI to v${latestVersion}\n`));
263
+ }
264
+ catch (err) {
265
+ const message = err instanceof Error ? err.message : String(err);
266
+ console.error(chalk.red(`Failed to update: ${message}\n`));
267
+ process.exit(1);
268
+ }
269
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * install-session-hooks — Wire up session-start hooks for coding CLIs.
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".
6
+ *
7
+ * Supported CLIs:
8
+ * - Claude Code: Native SessionStart hook in .claude/settings.json
9
+ * - Codex CLI: AGENTS.md instructions (no native hook support)
10
+ * - Gemini CLI: GEMINI.md instructions (no native hook support)
11
+ */
12
+ export interface InstallSessionOptions {
13
+ cli?: string;
14
+ }
15
+ export declare function installSessionHooks(options: InstallSessionOptions): Promise<void>;
16
+ export declare function sessionHooksStatus(): Promise<void>;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * install-session-hooks — Wire up session-start hooks for coding CLIs.
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".
6
+ *
7
+ * Supported CLIs:
8
+ * - Claude Code: Native SessionStart hook in .claude/settings.json
9
+ * - Codex CLI: AGENTS.md instructions (no native hook support)
10
+ * - Gemini CLI: GEMINI.md instructions (no native hook support)
11
+ */
12
+ import { execSync } from 'child_process';
13
+ import chalk from 'chalk';
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { findGitRoot } from '../utils/git.js';
17
+ // ============================================================================
18
+ // CLI detection (reused from triage runner pattern)
19
+ // ============================================================================
20
+ function isCLIInstalled(name) {
21
+ try {
22
+ execSync(process.platform === 'win32' ? `where ${name}` : `which ${name}`, {
23
+ stdio: 'ignore',
24
+ });
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function detectInstalledCLIs() {
32
+ const installed = [];
33
+ if (isCLIInstalled('claude'))
34
+ installed.push('claude');
35
+ if (isCLIInstalled('codex'))
36
+ installed.push('codex');
37
+ if (isCLIInstalled('gemini'))
38
+ installed.push('gemini');
39
+ return installed;
40
+ }
41
+ // ============================================================================
42
+ // Claude Code: Native SessionStart hook
43
+ // ============================================================================
44
+ const HAYSTACK_HOOK_COMMAND = 'haystack check-pending --hook';
45
+ function installClaudeHook(gitRoot) {
46
+ const settingsDir = join(gitRoot, '.claude');
47
+ const settingsPath = join(settingsDir, 'settings.json');
48
+ try {
49
+ // Ensure .claude directory exists
50
+ if (!existsSync(settingsDir)) {
51
+ mkdirSync(settingsDir, { recursive: true });
52
+ }
53
+ // Read or create settings
54
+ let settings = {};
55
+ if (existsSync(settingsPath)) {
56
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
57
+ }
58
+ // Ensure hooks object exists
59
+ const hooks = (settings.hooks || {});
60
+ settings.hooks = hooks;
61
+ // Ensure SessionStart array exists
62
+ const sessionStart = (hooks.SessionStart || []);
63
+ 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.'));
68
+ return true;
69
+ }
70
+ // Add the hook
71
+ sessionStart.push({
72
+ matcher: '',
73
+ hooks: [
74
+ {
75
+ type: 'command',
76
+ command: HAYSTACK_HOOK_COMMAND,
77
+ },
78
+ ],
79
+ });
80
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
81
+ return true;
82
+ }
83
+ catch (err) {
84
+ console.error(chalk.red(` Failed to update .claude/settings.json: ${err}`));
85
+ return false;
86
+ }
87
+ }
88
+ function checkClaudeHook(gitRoot) {
89
+ const settingsPath = join(gitRoot, '.claude', 'settings.json');
90
+ if (!existsSync(settingsPath))
91
+ return false;
92
+ try {
93
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
94
+ const sessionStart = settings?.hooks?.SessionStart || [];
95
+ return sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes('haystack check-pending')));
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ // ============================================================================
102
+ // Codex CLI: AGENTS.md instructions
103
+ // ============================================================================
104
+ const CODEX_HOOK_MARKER = '<!-- haystack-session-hook -->';
105
+ const CODEX_HOOK_BLOCK = `
106
+ ${CODEX_HOOK_MARKER}
107
+ ## Haystack Session Check
108
+
109
+ When starting a new session, run this command first to check for pending PR analysis results:
110
+
111
+ \`\`\`bash
112
+ haystack check-pending --hook
113
+ \`\`\`
114
+
115
+ This shows whether your submitted PRs are "Good to merge" or "Need your input".
116
+ `;
117
+ function installCodexHook(gitRoot) {
118
+ const agentsPath = join(gitRoot, 'AGENTS.md');
119
+ try {
120
+ let content = '';
121
+ if (existsSync(agentsPath)) {
122
+ content = readFileSync(agentsPath, 'utf-8');
123
+ if (content.includes(CODEX_HOOK_MARKER)) {
124
+ console.log(chalk.dim(' Codex AGENTS.md already has session hook.'));
125
+ return true;
126
+ }
127
+ }
128
+ content += CODEX_HOOK_BLOCK;
129
+ writeFileSync(agentsPath, content);
130
+ return true;
131
+ }
132
+ catch (err) {
133
+ console.error(chalk.red(` Failed to update AGENTS.md: ${err}`));
134
+ return false;
135
+ }
136
+ }
137
+ function checkCodexHook(gitRoot) {
138
+ const agentsPath = join(gitRoot, 'AGENTS.md');
139
+ if (!existsSync(agentsPath))
140
+ return false;
141
+ try {
142
+ const content = readFileSync(agentsPath, 'utf-8');
143
+ return content.includes(CODEX_HOOK_MARKER);
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ // ============================================================================
150
+ // Gemini CLI: GEMINI.md instructions
151
+ // ============================================================================
152
+ const GEMINI_HOOK_MARKER = '<!-- haystack-session-hook -->';
153
+ const GEMINI_HOOK_BLOCK = `
154
+ ${GEMINI_HOOK_MARKER}
155
+ ## Haystack Session Check
156
+
157
+ When starting a new session, run this command first to check for pending PR analysis results:
158
+
159
+ \`\`\`bash
160
+ haystack check-pending --hook
161
+ \`\`\`
162
+
163
+ This shows whether your submitted PRs are "Good to merge" or "Need your input".
164
+ `;
165
+ function installGeminiHook(gitRoot) {
166
+ const geminiPath = join(gitRoot, 'GEMINI.md');
167
+ try {
168
+ let content = '';
169
+ if (existsSync(geminiPath)) {
170
+ content = readFileSync(geminiPath, 'utf-8');
171
+ if (content.includes(GEMINI_HOOK_MARKER)) {
172
+ console.log(chalk.dim(' Gemini GEMINI.md already has session hook.'));
173
+ return true;
174
+ }
175
+ }
176
+ content += GEMINI_HOOK_BLOCK;
177
+ writeFileSync(geminiPath, content);
178
+ return true;
179
+ }
180
+ catch (err) {
181
+ console.error(chalk.red(` Failed to update GEMINI.md: ${err}`));
182
+ return false;
183
+ }
184
+ }
185
+ function checkGeminiHook(gitRoot) {
186
+ const geminiPath = join(gitRoot, 'GEMINI.md');
187
+ if (!existsSync(geminiPath))
188
+ return false;
189
+ try {
190
+ const content = readFileSync(geminiPath, 'utf-8');
191
+ return content.includes(GEMINI_HOOK_MARKER);
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ // ============================================================================
198
+ // CLI config registry
199
+ // ============================================================================
200
+ const CLI_HOOKS = {
201
+ claude: {
202
+ name: 'claude',
203
+ displayName: 'Claude Code',
204
+ hasNativeHook: true,
205
+ install: installClaudeHook,
206
+ check: checkClaudeHook,
207
+ },
208
+ codex: {
209
+ name: 'codex',
210
+ displayName: 'Codex CLI',
211
+ hasNativeHook: false,
212
+ install: installCodexHook,
213
+ check: checkCodexHook,
214
+ },
215
+ gemini: {
216
+ name: 'gemini',
217
+ displayName: 'Gemini CLI',
218
+ hasNativeHook: false,
219
+ install: installGeminiHook,
220
+ check: checkGeminiHook,
221
+ },
222
+ };
223
+ // ============================================================================
224
+ // Commands
225
+ // ============================================================================
226
+ export async function installSessionHooks(options) {
227
+ console.log(chalk.cyan('\nHaystack Session Hooks\n'));
228
+ const gitRoot = findGitRoot();
229
+ if (!gitRoot) {
230
+ console.error(chalk.red('Not a git repository.\n'));
231
+ process.exit(1);
232
+ }
233
+ // Determine which CLIs to install for
234
+ let targets;
235
+ if (options.cli) {
236
+ const cli = options.cli.toLowerCase();
237
+ if (cli === 'all') {
238
+ targets = detectInstalledCLIs();
239
+ }
240
+ else if (cli in CLI_HOOKS) {
241
+ targets = [cli];
242
+ }
243
+ else {
244
+ console.error(chalk.red(`Unknown CLI: ${options.cli}`));
245
+ console.log(chalk.dim('Supported: claude, codex, gemini, all'));
246
+ process.exit(1);
247
+ }
248
+ }
249
+ else {
250
+ // Auto-detect
251
+ targets = detectInstalledCLIs();
252
+ }
253
+ if (targets.length === 0) {
254
+ console.log(chalk.yellow('No supported coding CLI detected.\n'));
255
+ console.log(chalk.dim('Supported CLIs: Claude Code, Codex CLI, Gemini CLI'));
256
+ console.log(chalk.dim('Install one and try again, or specify --cli <name>.\n'));
257
+ return;
258
+ }
259
+ console.log(chalk.dim(`Installing for: ${targets.map(t => CLI_HOOKS[t].displayName).join(', ')}\n`));
260
+ for (const target of targets) {
261
+ const config = CLI_HOOKS[target];
262
+ const hookType = config.hasNativeHook ? 'native hook' : 'markdown instructions';
263
+ console.log(` Installing ${config.displayName} (${hookType})...`);
264
+ const success = config.install(gitRoot);
265
+ if (success) {
266
+ console.log(chalk.green(` ✓ ${config.displayName} configured`));
267
+ }
268
+ else {
269
+ console.log(chalk.red(` ✗ ${config.displayName} failed`));
270
+ }
271
+ }
272
+ console.log(chalk.green('\n✓ Session hooks installed!\n'));
273
+ console.log(chalk.dim('When you start a new session, you\'ll see:'));
274
+ console.log(chalk.green(' [Haystack] ✓ PR #42 "Fix auth": Good to merge'));
275
+ console.log(chalk.yellow(' [Haystack] ⚠ PR #37 "Refactor API": Needs your input (2 bugs)'));
276
+ console.log('');
277
+ }
278
+ export async function sessionHooksStatus() {
279
+ console.log(chalk.cyan('\nHaystack Session Hooks Status\n'));
280
+ const gitRoot = findGitRoot();
281
+ if (!gitRoot) {
282
+ console.error(chalk.red('Not a git repository.\n'));
283
+ process.exit(1);
284
+ }
285
+ const installed = detectInstalledCLIs();
286
+ for (const cli of ['claude', 'codex', 'gemini']) {
287
+ const config = CLI_HOOKS[cli];
288
+ const isInstalled = installed.includes(cli);
289
+ const hookConfigured = config.check(gitRoot);
290
+ const cliIcon = isInstalled ? chalk.green('✓') : chalk.dim('-');
291
+ const hookIcon = hookConfigured ? chalk.green('✓') : chalk.red('✗');
292
+ console.log(` ${cliIcon} ${config.displayName} ${isInstalled ? '(installed)' : '(not found)'}`);
293
+ if (isInstalled) {
294
+ console.log(` ${hookIcon} Session hook ${hookConfigured ? 'configured' : 'not configured'}`);
295
+ }
296
+ }
297
+ const anyMissing = installed.some(cli => !CLI_HOOKS[cli].check(gitRoot));
298
+ if (anyMissing) {
299
+ console.log(chalk.dim('\nRun: haystack hooks install-session'));
300
+ }
301
+ console.log('');
302
+ }
@@ -32,7 +32,7 @@ async function startDeviceFlow() {
32
32
  * Poll for access token
33
33
  */
34
34
  async function pollForToken(deviceCode, interval) {
35
- while (true) {
35
+ for (;;) {
36
36
  await new Promise(resolve => setTimeout(resolve, interval * 1000));
37
37
  const response = await fetch('https://github.com/login/oauth/access_token', {
38
38
  method: 'POST',
@@ -0,0 +1,31 @@
1
+ /**
2
+ * haystack policy - Manage review policies
3
+ *
4
+ * Commands:
5
+ * list - List all review policies
6
+ * add - Add a new policy interactively
7
+ * remove - Remove a policy by name
8
+ * init - Create initial review-policy.md
9
+ */
10
+ /**
11
+ * List all review policies
12
+ */
13
+ export declare function listPolicies(): Promise<void>;
14
+ /**
15
+ * Add a new review policy interactively
16
+ */
17
+ export declare function addPolicy(nameArg?: string): Promise<void>;
18
+ /**
19
+ * Remove a policy by name
20
+ */
21
+ export declare function removePolicy(name: string): Promise<void>;
22
+ /**
23
+ * Add a review instruction
24
+ */
25
+ export declare function addInstruction(text?: string): Promise<void>;
26
+ /**
27
+ * Create initial review-policy.md with example policies
28
+ */
29
+ export declare function initPolicies(options: {
30
+ force?: boolean;
31
+ }): Promise<void>;