hungry-ghost-hive 0.43.2 → 0.44.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 (73) hide show
  1. package/dist/agents/tech-lead.d.ts.map +1 -1
  2. package/dist/agents/tech-lead.js +4 -1
  3. package/dist/agents/tech-lead.js.map +1 -1
  4. package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
  5. package/dist/cli/commands/manager/tech-lead-lifecycle.js +4 -1
  6. package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
  7. package/dist/cli/commands/pr.js +5 -0
  8. package/dist/cli/commands/pr.js.map +1 -1
  9. package/dist/cli/commands/pr.test.js +43 -1
  10. package/dist/cli/commands/pr.test.js.map +1 -1
  11. package/dist/cli/commands/req.d.ts.map +1 -1
  12. package/dist/cli/commands/req.js +2 -1
  13. package/dist/cli/commands/req.js.map +1 -1
  14. package/dist/cli/commands/resume.d.ts.map +1 -1
  15. package/dist/cli/commands/resume.js +4 -1
  16. package/dist/cli/commands/resume.js.map +1 -1
  17. package/dist/cli-runtimes/chrome.d.ts +17 -0
  18. package/dist/cli-runtimes/chrome.d.ts.map +1 -0
  19. package/dist/cli-runtimes/chrome.js +36 -0
  20. package/dist/cli-runtimes/chrome.js.map +1 -0
  21. package/dist/cli-runtimes/claude.d.ts +3 -3
  22. package/dist/cli-runtimes/claude.d.ts.map +1 -1
  23. package/dist/cli-runtimes/claude.js +14 -8
  24. package/dist/cli-runtimes/claude.js.map +1 -1
  25. package/dist/cli-runtimes/codex.d.ts +3 -3
  26. package/dist/cli-runtimes/codex.d.ts.map +1 -1
  27. package/dist/cli-runtimes/codex.js +2 -2
  28. package/dist/cli-runtimes/codex.js.map +1 -1
  29. package/dist/cli-runtimes/gemini.d.ts +3 -3
  30. package/dist/cli-runtimes/gemini.d.ts.map +1 -1
  31. package/dist/cli-runtimes/gemini.js +2 -2
  32. package/dist/cli-runtimes/gemini.js.map +1 -1
  33. package/dist/cli-runtimes/index.d.ts +3 -2
  34. package/dist/cli-runtimes/index.d.ts.map +1 -1
  35. package/dist/cli-runtimes/index.js +1 -0
  36. package/dist/cli-runtimes/index.js.map +1 -1
  37. package/dist/cli-runtimes/index.test.js +133 -1
  38. package/dist/cli-runtimes/index.test.js.map +1 -1
  39. package/dist/cli-runtimes/types.d.ts +9 -2
  40. package/dist/cli-runtimes/types.d.ts.map +1 -1
  41. package/dist/config/schema.d.ts +8 -0
  42. package/dist/config/schema.d.ts.map +1 -1
  43. package/dist/config/schema.js +6 -0
  44. package/dist/config/schema.js.map +1 -1
  45. package/dist/context-files/index.test.js +1 -0
  46. package/dist/context-files/index.test.js.map +1 -1
  47. package/dist/orchestrator/scheduler.d.ts.map +1 -1
  48. package/dist/orchestrator/scheduler.js +4 -1
  49. package/dist/orchestrator/scheduler.js.map +1 -1
  50. package/dist/utils/auto-merge.d.ts.map +1 -1
  51. package/dist/utils/auto-merge.js +66 -5
  52. package/dist/utils/auto-merge.js.map +1 -1
  53. package/dist/utils/auto-merge.test.js +62 -0
  54. package/dist/utils/auto-merge.test.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/agents/tech-lead.ts +4 -1
  57. package/src/cli/commands/manager/tech-lead-lifecycle.ts +4 -1
  58. package/src/cli/commands/pr.test.ts +77 -1
  59. package/src/cli/commands/pr.ts +5 -0
  60. package/src/cli/commands/req.ts +4 -1
  61. package/src/cli/commands/resume.ts +4 -1
  62. package/src/cli-runtimes/chrome.ts +43 -0
  63. package/src/cli-runtimes/claude.ts +26 -9
  64. package/src/cli-runtimes/codex.ts +12 -3
  65. package/src/cli-runtimes/gemini.ts +12 -3
  66. package/src/cli-runtimes/index.test.ts +158 -0
  67. package/src/cli-runtimes/index.ts +3 -2
  68. package/src/cli-runtimes/types.ts +19 -2
  69. package/src/config/schema.ts +6 -0
  70. package/src/context-files/index.test.ts +1 -0
  71. package/src/orchestrator/scheduler.ts +9 -1
  72. package/src/utils/auto-merge.test.ts +81 -0
  73. package/src/utils/auto-merge.ts +78 -5
@@ -2,7 +2,11 @@
2
2
 
3
3
  import type { Command } from 'commander';
4
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
- import { getPullRequestById, updatePullRequest } from '../../db/queries/pull-requests.js';
5
+ import {
6
+ getOpenPullRequestsByStory,
7
+ getPullRequestById,
8
+ updatePullRequest,
9
+ } from '../../db/queries/pull-requests.js';
6
10
  import { autoMergeApprovedPRs } from '../../utils/auto-merge.js';
7
11
 
8
12
  // Mock dependencies
@@ -205,6 +209,78 @@ describe('pr command', () => {
205
209
  const fromOpt = submitCmd?.options.find(opt => opt.long === '--from');
206
210
  expect(fromOpt).toBeDefined();
207
211
  });
212
+
213
+ it('should auto-close existing PRs with different github_pr_number', async () => {
214
+ vi.mocked(getOpenPullRequestsByStory).mockReturnValue([
215
+ {
216
+ id: 'old-pr-1',
217
+ story_id: 'TEST-1',
218
+ team_id: 'team-1',
219
+ branch_name: 'feature/old-branch',
220
+ github_pr_number: 42,
221
+ github_pr_url: null,
222
+ submitted_by: null,
223
+ reviewed_by: null,
224
+ status: 'queued',
225
+ review_notes: null,
226
+ created_at: '2026-01-01T00:00:00.000Z',
227
+ updated_at: '2026-01-01T00:00:00.000Z',
228
+ reviewed_at: null,
229
+ },
230
+ ]);
231
+
232
+ await run(
233
+ 'submit',
234
+ '--branch',
235
+ 'feature/new-branch',
236
+ '--story',
237
+ 'TEST-1',
238
+ '--pr-number',
239
+ '99'
240
+ );
241
+
242
+ expect(updatePullRequest).toHaveBeenCalledWith(
243
+ expect.anything(),
244
+ 'old-pr-1',
245
+ expect.objectContaining({ status: 'closed' })
246
+ );
247
+ });
248
+
249
+ it('should skip auto-close when resubmitting same github PR number', async () => {
250
+ vi.mocked(getOpenPullRequestsByStory).mockReturnValue([
251
+ {
252
+ id: 'existing-pr-1',
253
+ story_id: 'TEST-1',
254
+ team_id: 'team-1',
255
+ branch_name: 'feature/same-branch',
256
+ github_pr_number: 55,
257
+ github_pr_url: null,
258
+ submitted_by: null,
259
+ reviewed_by: null,
260
+ status: 'queued',
261
+ review_notes: null,
262
+ created_at: '2026-01-01T00:00:00.000Z',
263
+ updated_at: '2026-01-01T00:00:00.000Z',
264
+ reviewed_at: null,
265
+ },
266
+ ]);
267
+
268
+ await run(
269
+ 'submit',
270
+ '--branch',
271
+ 'feature/same-branch',
272
+ '--story',
273
+ 'TEST-1',
274
+ '--pr-number',
275
+ '55'
276
+ );
277
+
278
+ expect(updatePullRequest).not.toHaveBeenCalledWith(
279
+ expect.anything(),
280
+ 'existing-pr-1',
281
+ expect.objectContaining({ status: 'closed' })
282
+ );
283
+ });
208
284
  });
209
285
 
210
286
  describe('queue subcommand', () => {
@@ -62,8 +62,13 @@ prCommand
62
62
  teamId = story.team_id;
63
63
 
64
64
  // Auto-close any existing open PRs for this story
65
+ const incomingPrNumber = options.prNumber ? parseInt(options.prNumber, 10) : null;
65
66
  const existingPRs = getOpenPullRequestsByStory(db.db, storyId);
66
67
  for (const existingPR of existingPRs) {
68
+ // Skip auto-close if this is a resubmit of the same GitHub PR
69
+ if (incomingPrNumber !== null && existingPR.github_pr_number === incomingPrNumber) {
70
+ continue;
71
+ }
67
72
  updatePullRequest(db.db, existingPR.id, { status: 'closed' });
68
73
  createLog(db.db, {
69
74
  agentId: options.from || 'system',
@@ -221,9 +221,12 @@ export const reqCommand = new Command('req')
221
221
 
222
222
  try {
223
223
  // Build CLI command using the configured runtime for Tech Lead
224
+ const chromeEnabled =
225
+ config.agents?.chrome_enabled === true && techLeadCliTool === 'claude';
224
226
  const commandArgs = getCliRuntimeBuilder(techLeadCliTool).buildSpawnCommand(
225
227
  techLeadModel,
226
- techLeadSafetyMode
228
+ techLeadSafetyMode,
229
+ { chrome: chromeEnabled }
227
230
  );
228
231
 
229
232
  // Pass the prompt as initialPrompt so it's included as a CLI positional
@@ -91,8 +91,11 @@ export const resumeCommand = new Command('resume')
91
91
  const model = resolveRuntimeModelForCli(selectedModel, cliTool);
92
92
 
93
93
  // Build resume command using CLI runtime builder
94
+ const chromeEnabled = config.agents?.chrome_enabled === true && cliTool === 'claude';
94
95
  const runtimeBuilder = getCliRuntimeBuilder(cliTool);
95
- const commandArgs = runtimeBuilder.buildResumeCommand(model, sessionName, safetyMode);
96
+ const commandArgs = runtimeBuilder.buildResumeCommand(model, sessionName, safetyMode, {
97
+ chrome: chromeEnabled,
98
+ });
96
99
 
97
100
  // Spawn new session
98
101
  await spawnTmuxSession({
@@ -0,0 +1,43 @@
1
+ // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
+
3
+ import { execa } from 'execa';
4
+ import type { CliRuntimeType } from './types.js';
5
+
6
+ /**
7
+ * Detect whether the Claude CLI supports the --chrome flag.
8
+ * Runs `claude --help` and checks if the output mentions --chrome.
9
+ * @returns true if --chrome is recognized by the CLI
10
+ */
11
+ export async function detectChromeAvailability(): Promise<boolean> {
12
+ try {
13
+ const result = await execa('claude', ['--help']);
14
+ const output = result.stdout + result.stderr;
15
+ return output.includes('--chrome');
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Resolve the effective chrome enabled state from config value.
23
+ * - true/false: use the explicit value
24
+ * - 'auto': detect availability, but only enable for claude CLI tool
25
+ * @param configValue - The chrome_enabled config value (true, false, or 'auto')
26
+ * @param cliTool - The CLI tool configured for the agent
27
+ * @returns Whether chrome should be enabled
28
+ */
29
+ export async function resolveChromeEnabled(
30
+ configValue: boolean | 'auto',
31
+ cliTool: CliRuntimeType
32
+ ): Promise<boolean> {
33
+ if (typeof configValue === 'boolean') {
34
+ return configValue;
35
+ }
36
+
37
+ // Auto-detect: only enable for claude CLI tool
38
+ if (cliTool !== 'claude') {
39
+ return false;
40
+ }
41
+
42
+ return detectChromeAvailability();
43
+ }
@@ -1,20 +1,37 @@
1
1
  // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
2
 
3
- import { CliRuntimeBuilder, RuntimeSafetyMode } from './types.js';
3
+ import { CliRuntimeBuilder, RuntimeOptions, RuntimeSafetyMode } from './types.js';
4
4
 
5
5
  export class ClaudeRuntimeBuilder implements CliRuntimeBuilder {
6
- buildSpawnCommand(model: string, safetyMode: RuntimeSafetyMode): string[] {
7
- if (safetyMode === 'safe') {
8
- return ['claude', '--model', model];
6
+ buildSpawnCommand(
7
+ model: string,
8
+ safetyMode: RuntimeSafetyMode,
9
+ options?: RuntimeOptions
10
+ ): string[] {
11
+ const args =
12
+ safetyMode === 'safe'
13
+ ? ['claude', '--model', model]
14
+ : ['claude', '--dangerously-skip-permissions', '--model', model];
15
+ if (options?.chrome) {
16
+ args.push('--chrome');
9
17
  }
10
- return ['claude', '--dangerously-skip-permissions', '--model', model];
18
+ return args;
11
19
  }
12
20
 
13
- buildResumeCommand(model: string, sessionId: string, safetyMode: RuntimeSafetyMode): string[] {
14
- if (safetyMode === 'safe') {
15
- return ['claude', '--model', model, '--resume', sessionId];
21
+ buildResumeCommand(
22
+ model: string,
23
+ sessionId: string,
24
+ safetyMode: RuntimeSafetyMode,
25
+ options?: RuntimeOptions
26
+ ): string[] {
27
+ const args =
28
+ safetyMode === 'safe'
29
+ ? ['claude', '--model', model, '--resume', sessionId]
30
+ : ['claude', '--dangerously-skip-permissions', '--model', model, '--resume', sessionId];
31
+ if (options?.chrome) {
32
+ args.push('--chrome');
16
33
  }
17
- return ['claude', '--dangerously-skip-permissions', '--model', model, '--resume', sessionId];
34
+ return args;
18
35
  }
19
36
 
20
37
  getAutoApprovalFlag(safetyMode: RuntimeSafetyMode): string {
@@ -1,9 +1,13 @@
1
1
  // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
2
 
3
- import { CliRuntimeBuilder, RuntimeSafetyMode } from './types.js';
3
+ import { CliRuntimeBuilder, RuntimeOptions, RuntimeSafetyMode } from './types.js';
4
4
 
5
5
  export class CodexRuntimeBuilder implements CliRuntimeBuilder {
6
- buildSpawnCommand(model: string, safetyMode: RuntimeSafetyMode): string[] {
6
+ buildSpawnCommand(
7
+ model: string,
8
+ safetyMode: RuntimeSafetyMode,
9
+ _options?: RuntimeOptions
10
+ ): string[] {
7
11
  const approvalPolicy = safetyMode === 'safe' ? 'on-request' : 'never';
8
12
  const sandboxMode = safetyMode === 'safe' ? 'workspace-write' : 'danger-full-access';
9
13
  return [
@@ -17,7 +21,12 @@ export class CodexRuntimeBuilder implements CliRuntimeBuilder {
17
21
  ];
18
22
  }
19
23
 
20
- buildResumeCommand(model: string, sessionId: string, safetyMode: RuntimeSafetyMode): string[] {
24
+ buildResumeCommand(
25
+ model: string,
26
+ sessionId: string,
27
+ safetyMode: RuntimeSafetyMode,
28
+ _options?: RuntimeOptions
29
+ ): string[] {
21
30
  const approvalPolicy = safetyMode === 'safe' ? 'on-request' : 'never';
22
31
  const sandboxMode = safetyMode === 'safe' ? 'workspace-write' : 'danger-full-access';
23
32
  return [
@@ -1,14 +1,23 @@
1
1
  // Licensed under the Hungry Ghost Hive License. See LICENSE.
2
2
 
3
- import { CliRuntimeBuilder, RuntimeSafetyMode } from './types.js';
3
+ import { CliRuntimeBuilder, RuntimeOptions, RuntimeSafetyMode } from './types.js';
4
4
 
5
5
  export class GeminiRuntimeBuilder implements CliRuntimeBuilder {
6
- buildSpawnCommand(model: string, safetyMode: RuntimeSafetyMode): string[] {
6
+ buildSpawnCommand(
7
+ model: string,
8
+ safetyMode: RuntimeSafetyMode,
9
+ _options?: RuntimeOptions
10
+ ): string[] {
7
11
  const sandboxMode = safetyMode === 'safe' ? 'workspace-write' : 'none';
8
12
  return ['gemini', '--model', model, '--sandbox', sandboxMode];
9
13
  }
10
14
 
11
- buildResumeCommand(model: string, sessionId: string, safetyMode: RuntimeSafetyMode): string[] {
15
+ buildResumeCommand(
16
+ model: string,
17
+ sessionId: string,
18
+ safetyMode: RuntimeSafetyMode,
19
+ _options?: RuntimeOptions
20
+ ): string[] {
12
21
  const sandboxMode = safetyMode === 'safe' ? 'workspace-write' : 'none';
13
22
  return ['gemini', '--model', model, '--sandbox', sandboxMode, '--resume', sessionId];
14
23
  }
@@ -5,7 +5,9 @@ import {
5
5
  ClaudeRuntimeBuilder,
6
6
  CodexRuntimeBuilder,
7
7
  GeminiRuntimeBuilder,
8
+ detectChromeAvailability,
8
9
  getCliRuntimeBuilder,
10
+ resolveChromeEnabled,
9
11
  resolveRuntimeModelForCli,
10
12
  selectCompatibleModelForCli,
11
13
  validateCliBinary,
@@ -506,4 +508,160 @@ describe('CLI Runtime Builders', () => {
506
508
  expect(selected).toBe('claude-sonnet-4-5-20250929');
507
509
  });
508
510
  });
511
+
512
+ describe('detectChromeAvailability', () => {
513
+ beforeEach(() => {
514
+ vi.clearAllMocks();
515
+ });
516
+
517
+ afterEach(() => {
518
+ vi.restoreAllMocks();
519
+ });
520
+
521
+ it('should return true when claude --help output includes --chrome', async () => {
522
+ const { execa } = await import('execa');
523
+ vi.mocked(execa).mockResolvedValue({
524
+ stdout: 'Usage: claude [options]\n --chrome Enable Chrome integration\n',
525
+ stderr: '',
526
+ exitCode: 0,
527
+ command: 'claude --help',
528
+ escapedCommand: 'claude --help',
529
+ failed: false,
530
+ timedOut: false,
531
+ isCanceled: false,
532
+ killed: false,
533
+ } as any);
534
+
535
+ const result = await detectChromeAvailability();
536
+ expect(result).toBe(true);
537
+ });
538
+
539
+ it('should return false when claude --help output does not include --chrome', async () => {
540
+ const { execa } = await import('execa');
541
+ vi.mocked(execa).mockResolvedValue({
542
+ stdout: 'Usage: claude [options]\n --model Set the model\n',
543
+ stderr: '',
544
+ exitCode: 0,
545
+ command: 'claude --help',
546
+ escapedCommand: 'claude --help',
547
+ failed: false,
548
+ timedOut: false,
549
+ isCanceled: false,
550
+ killed: false,
551
+ } as any);
552
+
553
+ const result = await detectChromeAvailability();
554
+ expect(result).toBe(false);
555
+ });
556
+
557
+ it('should return true when --chrome appears in stderr', async () => {
558
+ const { execa } = await import('execa');
559
+ vi.mocked(execa).mockResolvedValue({
560
+ stdout: '',
561
+ stderr: 'Options:\n --chrome Enable Chrome integration\n',
562
+ exitCode: 0,
563
+ command: 'claude --help',
564
+ escapedCommand: 'claude --help',
565
+ failed: false,
566
+ timedOut: false,
567
+ isCanceled: false,
568
+ killed: false,
569
+ } as any);
570
+
571
+ const result = await detectChromeAvailability();
572
+ expect(result).toBe(true);
573
+ });
574
+
575
+ it('should return false when the command fails', async () => {
576
+ const { execa } = await import('execa');
577
+ vi.mocked(execa).mockRejectedValue(new Error('Command failed'));
578
+
579
+ const result = await detectChromeAvailability();
580
+ expect(result).toBe(false);
581
+ });
582
+ });
583
+
584
+ describe('resolveChromeEnabled', () => {
585
+ beforeEach(() => {
586
+ vi.clearAllMocks();
587
+ });
588
+
589
+ afterEach(() => {
590
+ vi.restoreAllMocks();
591
+ });
592
+
593
+ it('should return true when configValue is explicitly true', async () => {
594
+ const result = await resolveChromeEnabled(true, 'claude');
595
+ expect(result).toBe(true);
596
+ });
597
+
598
+ it('should return true when configValue is explicitly true for non-claude tool', async () => {
599
+ const result = await resolveChromeEnabled(true, 'codex');
600
+ expect(result).toBe(true);
601
+ });
602
+
603
+ it('should return false when configValue is explicitly false', async () => {
604
+ const result = await resolveChromeEnabled(false, 'claude');
605
+ expect(result).toBe(false);
606
+ });
607
+
608
+ it('should return false when configValue is explicitly false for non-claude tool', async () => {
609
+ const result = await resolveChromeEnabled(false, 'gemini');
610
+ expect(result).toBe(false);
611
+ });
612
+
613
+ it('should return false for auto mode with codex CLI tool', async () => {
614
+ const result = await resolveChromeEnabled('auto', 'codex');
615
+ expect(result).toBe(false);
616
+ });
617
+
618
+ it('should return false for auto mode with gemini CLI tool', async () => {
619
+ const result = await resolveChromeEnabled('auto', 'gemini');
620
+ expect(result).toBe(false);
621
+ });
622
+
623
+ it('should detect chrome availability for auto mode with claude CLI tool when --chrome is available', async () => {
624
+ const { execa } = await import('execa');
625
+ vi.mocked(execa).mockResolvedValue({
626
+ stdout: 'Usage: claude [options]\n --chrome Enable Chrome integration\n',
627
+ stderr: '',
628
+ exitCode: 0,
629
+ command: 'claude --help',
630
+ escapedCommand: 'claude --help',
631
+ failed: false,
632
+ timedOut: false,
633
+ isCanceled: false,
634
+ killed: false,
635
+ } as any);
636
+
637
+ const result = await resolveChromeEnabled('auto', 'claude');
638
+ expect(result).toBe(true);
639
+ });
640
+
641
+ it('should return false for auto mode with claude CLI tool when --chrome is not available', async () => {
642
+ const { execa } = await import('execa');
643
+ vi.mocked(execa).mockResolvedValue({
644
+ stdout: 'Usage: claude [options]\n --model Set the model\n',
645
+ stderr: '',
646
+ exitCode: 0,
647
+ command: 'claude --help',
648
+ escapedCommand: 'claude --help',
649
+ failed: false,
650
+ timedOut: false,
651
+ isCanceled: false,
652
+ killed: false,
653
+ } as any);
654
+
655
+ const result = await resolveChromeEnabled('auto', 'claude');
656
+ expect(result).toBe(false);
657
+ });
658
+
659
+ it('should return false for auto mode with claude CLI tool when detection fails', async () => {
660
+ const { execa } = await import('execa');
661
+ vi.mocked(execa).mockRejectedValue(new Error('Command not found'));
662
+
663
+ const result = await resolveChromeEnabled('auto', 'claude');
664
+ expect(result).toBe(false);
665
+ });
666
+ });
509
667
  });
@@ -5,7 +5,7 @@ import { UnsupportedFeatureError, ValidationError } from '../errors/index.js';
5
5
  import { ClaudeRuntimeBuilder } from './claude.js';
6
6
  import { CodexRuntimeBuilder } from './codex.js';
7
7
  import { GeminiRuntimeBuilder } from './gemini.js';
8
- import { CliRuntimeBuilder, CliRuntimeType, RuntimeSafetyMode } from './types.js';
8
+ import { CliRuntimeBuilder, CliRuntimeType, RuntimeOptions, RuntimeSafetyMode } from './types.js';
9
9
 
10
10
  const CODEX_CHATGPT_SAFE_MODEL = 'gpt-5.2-codex';
11
11
 
@@ -144,7 +144,8 @@ export function resolveRuntimeModelForCli(model: string, cliTool: CliRuntimeType
144
144
  return model;
145
145
  }
146
146
 
147
+ export { detectChromeAvailability, resolveChromeEnabled } from './chrome.js';
147
148
  export { ClaudeRuntimeBuilder } from './claude.js';
148
149
  export { CodexRuntimeBuilder } from './codex.js';
149
150
  export { GeminiRuntimeBuilder } from './gemini.js';
150
- export type { CliRuntimeBuilder, CliRuntimeType, RuntimeSafetyMode };
151
+ export type { CliRuntimeBuilder, CliRuntimeType, RuntimeOptions, RuntimeSafetyMode };
@@ -3,21 +3,38 @@
3
3
  export type CliRuntimeType = 'claude' | 'codex' | 'gemini';
4
4
  export type RuntimeSafetyMode = 'safe' | 'unsafe';
5
5
 
6
+ export interface RuntimeOptions {
7
+ chrome?: boolean;
8
+ }
9
+
6
10
  export interface CliRuntimeBuilder {
7
11
  /**
8
12
  * Build command array for spawning a new agent session
9
13
  * @param model - The model identifier to use
14
+ * @param safetyMode - The safety mode for the agent
15
+ * @param options - Optional runtime options (e.g., chrome flag)
10
16
  * @returns Array of command and arguments suitable for spawn
11
17
  */
12
- buildSpawnCommand(model: string, safetyMode: RuntimeSafetyMode): string[];
18
+ buildSpawnCommand(
19
+ model: string,
20
+ safetyMode: RuntimeSafetyMode,
21
+ options?: RuntimeOptions
22
+ ): string[];
13
23
 
14
24
  /**
15
25
  * Build command array for resuming an existing agent session
16
26
  * @param model - The model identifier to use
17
27
  * @param sessionId - The session ID to resume
28
+ * @param safetyMode - The safety mode for the agent
29
+ * @param options - Optional runtime options (e.g., chrome flag)
18
30
  * @returns Array of command and arguments suitable for spawn
19
31
  */
20
- buildResumeCommand(model: string, sessionId: string, safetyMode: RuntimeSafetyMode): string[];
32
+ buildResumeCommand(
33
+ model: string,
34
+ sessionId: string,
35
+ safetyMode: RuntimeSafetyMode,
36
+ options?: RuntimeOptions
37
+ ): string[];
21
38
 
22
39
  /**
23
40
  * Get the auto-approval flag for this CLI runtime
@@ -210,6 +210,9 @@ const AgentsConfigSchema = z.object({
210
210
  llm_timeout_ms: z.number().int().positive().default(1800000),
211
211
  // Max retries for LLM calls on timeout
212
212
  llm_max_retries: z.number().int().nonnegative().default(2),
213
+ // Enable Chrome browser automation via Claude in Chrome extension
214
+ // true = always enable, false = always disable, 'auto' = detect availability
215
+ chrome_enabled: z.union([z.boolean(), z.literal('auto')]).default('auto'),
213
216
  });
214
217
 
215
218
  // Manager daemon configuration
@@ -521,6 +524,9 @@ agents:
521
524
  llm_timeout_ms: 1800000
522
525
  # Max retries for LLM calls on timeout
523
526
  llm_max_retries: 2
527
+ # Enable Chrome browser automation (true, false, or auto)
528
+ # auto = detect if Claude CLI supports --chrome flag
529
+ chrome_enabled: auto
524
530
 
525
531
  # Manager daemon (micromanager nudge behavior)
526
532
  manager:
@@ -205,6 +205,7 @@ describe('context-files module', () => {
205
205
  checkpoint_threshold: 14000,
206
206
  llm_timeout_ms: 1800000,
207
207
  llm_max_retries: 2,
208
+ chrome_enabled: 'auto',
208
209
  },
209
210
  manager: {
210
211
  fast_poll_interval: 15000,
@@ -1052,7 +1052,15 @@ export class Scheduler {
1052
1052
  }
1053
1053
 
1054
1054
  // Build CLI command using the configured runtime
1055
- const commandArgs = getCliRuntimeBuilder(cliTool).buildSpawnCommand(runtimeModel, safetyMode);
1055
+ const chromeEnabled =
1056
+ this.config.hiveConfig?.agents?.chrome_enabled === true && cliTool === 'claude';
1057
+ const commandArgs = getCliRuntimeBuilder(cliTool).buildSpawnCommand(
1058
+ runtimeModel,
1059
+ safetyMode,
1060
+ {
1061
+ chrome: chromeEnabled,
1062
+ }
1063
+ );
1056
1064
 
1057
1065
  // Pass the prompt as initialPrompt so it's included as a CLI positional
1058
1066
  // argument via $(cat ...). This delivers the full multi-line prompt
@@ -21,6 +21,11 @@ vi.mock('./paths.js', () => ({
21
21
  getHivePaths: vi.fn(() => ({ hiveDir: '/mock/hive' })),
22
22
  }));
23
23
 
24
+ vi.mock('../connectors/project-management/operations.js', () => ({
25
+ postLifecycleComment: vi.fn().mockResolvedValue(undefined),
26
+ syncStatusForStory: vi.fn(),
27
+ }));
28
+
24
29
  import { loadConfig } from '../config/loader.js';
25
30
  import { autoMergeApprovedPRs } from './auto-merge.js';
26
31
 
@@ -266,6 +271,82 @@ describe('auto-merge functionality', () => {
266
271
  expect(mockLoadConfig).toHaveBeenCalledWith('/mock/hive');
267
272
  });
268
273
 
274
+ it('should keep PR as queued when auto-merge is pending (PR still open after gh pr merge --auto)', async () => {
275
+ const pr = createPullRequest(db, {
276
+ storyId,
277
+ teamId,
278
+ branchName: 'feature/auto-merge-pending',
279
+ githubPrNumber: 456,
280
+ });
281
+ updatePullRequest(db, pr.id, { status: 'approved' });
282
+
283
+ mockLoadConfig.mockReturnValue({
284
+ integrations: {
285
+ autonomy: { level: 'full' },
286
+ source_control: { provider: 'github' },
287
+ project_management: { provider: 'none' },
288
+ },
289
+ } as any);
290
+
291
+ // Mock execSync: first call returns OPEN+MERGEABLE, merge command succeeds,
292
+ // second call (post-merge check) returns OPEN (auto-merge pending)
293
+ const mockExecSync = vi.fn();
294
+ mockExecSync
295
+ .mockReturnValueOnce(
296
+ JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'CLEAN' })
297
+ )
298
+ .mockReturnValueOnce(undefined) // gh pr merge --auto
299
+ .mockReturnValueOnce(
300
+ JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'BLOCKED' })
301
+ );
302
+
303
+ vi.doMock('child_process', () => ({ execSync: mockExecSync }));
304
+
305
+ const dbClient = { db, save: vi.fn(), close: vi.fn(), runMigrations: vi.fn() };
306
+ const result = await autoMergeApprovedPRs('/mock/root', dbClient);
307
+
308
+ // Should return 0 because the PR was not actually merged yet
309
+ expect(result).toBe(0);
310
+ // PR should remain 'queued' (not rolled back to 'approved' or advanced to 'merged')
311
+ expect(getPullRequestById(db, pr.id)?.status).toBe('queued');
312
+ });
313
+
314
+ it('should reset stale branch PR to approved after updating behind branch', async () => {
315
+ const pr = createPullRequest(db, {
316
+ storyId,
317
+ teamId,
318
+ branchName: 'feature/stale-branch',
319
+ githubPrNumber: 789,
320
+ });
321
+ updatePullRequest(db, pr.id, { status: 'approved' });
322
+
323
+ mockLoadConfig.mockReturnValue({
324
+ integrations: {
325
+ autonomy: { level: 'full' },
326
+ source_control: { provider: 'github' },
327
+ project_management: { provider: 'none' },
328
+ },
329
+ } as any);
330
+
331
+ // Mock execSync: PR state shows BEHIND, then gh pr update-branch succeeds
332
+ const mockExecSync = vi.fn();
333
+ mockExecSync
334
+ .mockReturnValueOnce(
335
+ JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'BEHIND' })
336
+ )
337
+ .mockReturnValueOnce(undefined); // gh pr update-branch
338
+
339
+ vi.doMock('child_process', () => ({ execSync: mockExecSync }));
340
+
341
+ const dbClient = { db, save: vi.fn(), close: vi.fn(), runMigrations: vi.fn() };
342
+ const result = await autoMergeApprovedPRs('/mock/root', dbClient);
343
+
344
+ // Should return 0 because no merge happened yet
345
+ expect(result).toBe(0);
346
+ // PR should be reset to 'approved' to be retried on next cycle
347
+ expect(getPullRequestById(db, pr.id)?.status).toBe('approved');
348
+ });
349
+
269
350
  it('should skip approved PRs marked for manual merge', async () => {
270
351
  const pr = createPullRequest(db, {
271
352
  storyId,