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.
- package/dist/agents/tech-lead.d.ts.map +1 -1
- package/dist/agents/tech-lead.js +4 -1
- package/dist/agents/tech-lead.js.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.js +4 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
- package/dist/cli/commands/pr.js +5 -0
- package/dist/cli/commands/pr.js.map +1 -1
- package/dist/cli/commands/pr.test.js +43 -1
- package/dist/cli/commands/pr.test.js.map +1 -1
- package/dist/cli/commands/req.d.ts.map +1 -1
- package/dist/cli/commands/req.js +2 -1
- package/dist/cli/commands/req.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +4 -1
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli-runtimes/chrome.d.ts +17 -0
- package/dist/cli-runtimes/chrome.d.ts.map +1 -0
- package/dist/cli-runtimes/chrome.js +36 -0
- package/dist/cli-runtimes/chrome.js.map +1 -0
- package/dist/cli-runtimes/claude.d.ts +3 -3
- package/dist/cli-runtimes/claude.d.ts.map +1 -1
- package/dist/cli-runtimes/claude.js +14 -8
- package/dist/cli-runtimes/claude.js.map +1 -1
- package/dist/cli-runtimes/codex.d.ts +3 -3
- package/dist/cli-runtimes/codex.d.ts.map +1 -1
- package/dist/cli-runtimes/codex.js +2 -2
- package/dist/cli-runtimes/codex.js.map +1 -1
- package/dist/cli-runtimes/gemini.d.ts +3 -3
- package/dist/cli-runtimes/gemini.d.ts.map +1 -1
- package/dist/cli-runtimes/gemini.js +2 -2
- package/dist/cli-runtimes/gemini.js.map +1 -1
- package/dist/cli-runtimes/index.d.ts +3 -2
- package/dist/cli-runtimes/index.d.ts.map +1 -1
- package/dist/cli-runtimes/index.js +1 -0
- package/dist/cli-runtimes/index.js.map +1 -1
- package/dist/cli-runtimes/index.test.js +133 -1
- package/dist/cli-runtimes/index.test.js.map +1 -1
- package/dist/cli-runtimes/types.d.ts +9 -2
- package/dist/cli-runtimes/types.d.ts.map +1 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/context-files/index.test.js +1 -0
- package/dist/context-files/index.test.js.map +1 -1
- package/dist/orchestrator/scheduler.d.ts.map +1 -1
- package/dist/orchestrator/scheduler.js +4 -1
- package/dist/orchestrator/scheduler.js.map +1 -1
- package/dist/utils/auto-merge.d.ts.map +1 -1
- package/dist/utils/auto-merge.js +66 -5
- package/dist/utils/auto-merge.js.map +1 -1
- package/dist/utils/auto-merge.test.js +62 -0
- package/dist/utils/auto-merge.test.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/tech-lead.ts +4 -1
- package/src/cli/commands/manager/tech-lead-lifecycle.ts +4 -1
- package/src/cli/commands/pr.test.ts +77 -1
- package/src/cli/commands/pr.ts +5 -0
- package/src/cli/commands/req.ts +4 -1
- package/src/cli/commands/resume.ts +4 -1
- package/src/cli-runtimes/chrome.ts +43 -0
- package/src/cli-runtimes/claude.ts +26 -9
- package/src/cli-runtimes/codex.ts +12 -3
- package/src/cli-runtimes/gemini.ts +12 -3
- package/src/cli-runtimes/index.test.ts +158 -0
- package/src/cli-runtimes/index.ts +3 -2
- package/src/cli-runtimes/types.ts +19 -2
- package/src/config/schema.ts +6 -0
- package/src/context-files/index.test.ts +1 -0
- package/src/orchestrator/scheduler.ts +9 -1
- package/src/utils/auto-merge.test.ts +81 -0
- 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 {
|
|
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', () => {
|
package/src/cli/commands/pr.ts
CHANGED
|
@@ -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',
|
package/src/cli/commands/req.ts
CHANGED
|
@@ -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(
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
18
|
+
return args;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
buildResumeCommand(
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
package/src/config/schema.ts
CHANGED
|
@@ -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:
|
|
@@ -1052,7 +1052,15 @@ export class Scheduler {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
1054
|
// Build CLI command using the configured runtime
|
|
1055
|
-
const
|
|
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,
|