hungry-ghost-hive 0.43.2 → 0.45.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 (201) hide show
  1. package/dist/agents/base-agent.d.ts +1 -0
  2. package/dist/agents/base-agent.d.ts.map +1 -1
  3. package/dist/agents/base-agent.js +4 -0
  4. package/dist/agents/base-agent.js.map +1 -1
  5. package/dist/agents/intermediate.js +2 -2
  6. package/dist/agents/intermediate.js.map +1 -1
  7. package/dist/agents/junior.js +2 -2
  8. package/dist/agents/junior.js.map +1 -1
  9. package/dist/agents/qa.d.ts.map +1 -1
  10. package/dist/agents/qa.js +5 -5
  11. package/dist/agents/qa.js.map +1 -1
  12. package/dist/agents/senior.d.ts.map +1 -1
  13. package/dist/agents/senior.js +5 -5
  14. package/dist/agents/senior.js.map +1 -1
  15. package/dist/agents/tech-lead.d.ts.map +1 -1
  16. package/dist/agents/tech-lead.js +8 -3
  17. package/dist/agents/tech-lead.js.map +1 -1
  18. package/dist/cli/commands/assign.d.ts.map +1 -1
  19. package/dist/cli/commands/assign.js +4 -2
  20. package/dist/cli/commands/assign.js.map +1 -1
  21. package/dist/cli/commands/assign.test.js +5 -0
  22. package/dist/cli/commands/assign.test.js.map +1 -1
  23. package/dist/cli/commands/manager/handoff-recovery.d.ts.map +1 -1
  24. package/dist/cli/commands/manager/handoff-recovery.js +4 -2
  25. package/dist/cli/commands/manager/handoff-recovery.js.map +1 -1
  26. package/dist/cli/commands/manager/index.d.ts.map +1 -1
  27. package/dist/cli/commands/manager/index.js +16 -12
  28. package/dist/cli/commands/manager/index.js.map +1 -1
  29. package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
  30. package/dist/cli/commands/manager/tech-lead-lifecycle.js +8 -3
  31. package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
  32. package/dist/cli/commands/msg.d.ts.map +1 -1
  33. package/dist/cli/commands/msg.js +8 -7
  34. package/dist/cli/commands/msg.js.map +1 -1
  35. package/dist/cli/commands/my-stories.js +3 -3
  36. package/dist/cli/commands/my-stories.js.map +1 -1
  37. package/dist/cli/commands/nuke.d.ts.map +1 -1
  38. package/dist/cli/commands/nuke.js +18 -7
  39. package/dist/cli/commands/nuke.js.map +1 -1
  40. package/dist/cli/commands/nuke.test.js +24 -0
  41. package/dist/cli/commands/nuke.test.js.map +1 -1
  42. package/dist/cli/commands/pr.js +5 -0
  43. package/dist/cli/commands/pr.js.map +1 -1
  44. package/dist/cli/commands/pr.test.js +43 -1
  45. package/dist/cli/commands/pr.test.js.map +1 -1
  46. package/dist/cli/commands/req.d.ts +1 -1
  47. package/dist/cli/commands/req.d.ts.map +1 -1
  48. package/dist/cli/commands/req.js +9 -6
  49. package/dist/cli/commands/req.js.map +1 -1
  50. package/dist/cli/commands/resume.d.ts.map +1 -1
  51. package/dist/cli/commands/resume.js +4 -1
  52. package/dist/cli/commands/resume.js.map +1 -1
  53. package/dist/cli/commands/stories.js +3 -3
  54. package/dist/cli/commands/stories.js.map +1 -1
  55. package/dist/cli/dashboard/panels/agents.d.ts.map +1 -1
  56. package/dist/cli/dashboard/panels/agents.js +7 -3
  57. package/dist/cli/dashboard/panels/agents.js.map +1 -1
  58. package/dist/cli-runtimes/chrome.d.ts +17 -0
  59. package/dist/cli-runtimes/chrome.d.ts.map +1 -0
  60. package/dist/cli-runtimes/chrome.js +36 -0
  61. package/dist/cli-runtimes/chrome.js.map +1 -0
  62. package/dist/cli-runtimes/claude.d.ts +3 -3
  63. package/dist/cli-runtimes/claude.d.ts.map +1 -1
  64. package/dist/cli-runtimes/claude.js +14 -8
  65. package/dist/cli-runtimes/claude.js.map +1 -1
  66. package/dist/cli-runtimes/codex.d.ts +3 -3
  67. package/dist/cli-runtimes/codex.d.ts.map +1 -1
  68. package/dist/cli-runtimes/codex.js +2 -2
  69. package/dist/cli-runtimes/codex.js.map +1 -1
  70. package/dist/cli-runtimes/gemini.d.ts +3 -3
  71. package/dist/cli-runtimes/gemini.d.ts.map +1 -1
  72. package/dist/cli-runtimes/gemini.js +2 -2
  73. package/dist/cli-runtimes/gemini.js.map +1 -1
  74. package/dist/cli-runtimes/index.d.ts +3 -2
  75. package/dist/cli-runtimes/index.d.ts.map +1 -1
  76. package/dist/cli-runtimes/index.js +1 -0
  77. package/dist/cli-runtimes/index.js.map +1 -1
  78. package/dist/cli-runtimes/index.test.js +133 -1
  79. package/dist/cli-runtimes/index.test.js.map +1 -1
  80. package/dist/cli-runtimes/types.d.ts +9 -2
  81. package/dist/cli-runtimes/types.d.ts.map +1 -1
  82. package/dist/config/schema.d.ts +8 -0
  83. package/dist/config/schema.d.ts.map +1 -1
  84. package/dist/config/schema.js +6 -0
  85. package/dist/config/schema.js.map +1 -1
  86. package/dist/context-files/generator.d.ts +1 -1
  87. package/dist/context-files/generator.d.ts.map +1 -1
  88. package/dist/context-files/generator.js +3 -2
  89. package/dist/context-files/generator.js.map +1 -1
  90. package/dist/context-files/index.test.js +2 -0
  91. package/dist/context-files/index.test.js.map +1 -1
  92. package/dist/db/client.d.ts +1 -0
  93. package/dist/db/client.d.ts.map +1 -1
  94. package/dist/db/client.js +6 -0
  95. package/dist/db/client.js.map +1 -1
  96. package/dist/db/migrations/015-add-story-markdown-path.sql +5 -0
  97. package/dist/db/queries/stories.d.ts +3 -3
  98. package/dist/db/queries/stories.d.ts.map +1 -1
  99. package/dist/db/queries/stories.js +23 -5
  100. package/dist/db/queries/stories.js.map +1 -1
  101. package/dist/db/queries/test-helpers.d.ts.map +1 -1
  102. package/dist/db/queries/test-helpers.js +1 -0
  103. package/dist/db/queries/test-helpers.js.map +1 -1
  104. package/dist/git/worktree.d.ts.map +1 -1
  105. package/dist/git/worktree.js +7 -0
  106. package/dist/git/worktree.js.map +1 -1
  107. package/dist/git/worktree.test.js +30 -0
  108. package/dist/git/worktree.test.js.map +1 -1
  109. package/dist/orchestrator/prompt-templates.d.ts +3 -1
  110. package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
  111. package/dist/orchestrator/prompt-templates.js +16 -8
  112. package/dist/orchestrator/prompt-templates.js.map +1 -1
  113. package/dist/orchestrator/prompt-templates.test.js +4 -0
  114. package/dist/orchestrator/prompt-templates.test.js.map +1 -1
  115. package/dist/orchestrator/scheduler.d.ts.map +1 -1
  116. package/dist/orchestrator/scheduler.js +23 -12
  117. package/dist/orchestrator/scheduler.js.map +1 -1
  118. package/dist/orchestrator/scheduler.test.js +1 -0
  119. package/dist/orchestrator/scheduler.test.js.map +1 -1
  120. package/dist/tmux/manager.d.ts +7 -6
  121. package/dist/tmux/manager.d.ts.map +1 -1
  122. package/dist/tmux/manager.js +29 -13
  123. package/dist/tmux/manager.js.map +1 -1
  124. package/dist/utils/auto-merge.d.ts.map +1 -1
  125. package/dist/utils/auto-merge.js +66 -5
  126. package/dist/utils/auto-merge.js.map +1 -1
  127. package/dist/utils/auto-merge.test.js +62 -0
  128. package/dist/utils/auto-merge.test.js.map +1 -1
  129. package/dist/utils/instance.d.ts +32 -0
  130. package/dist/utils/instance.d.ts.map +1 -0
  131. package/dist/utils/instance.js +82 -0
  132. package/dist/utils/instance.js.map +1 -0
  133. package/dist/utils/instance.test.d.ts +2 -0
  134. package/dist/utils/instance.test.d.ts.map +1 -0
  135. package/dist/utils/instance.test.js +103 -0
  136. package/dist/utils/instance.test.js.map +1 -0
  137. package/dist/utils/paths.d.ts +2 -0
  138. package/dist/utils/paths.d.ts.map +1 -1
  139. package/dist/utils/paths.js +2 -0
  140. package/dist/utils/paths.js.map +1 -1
  141. package/dist/utils/paths.test.js +6 -0
  142. package/dist/utils/paths.test.js.map +1 -1
  143. package/dist/utils/story-markdown.d.ts +16 -0
  144. package/dist/utils/story-markdown.d.ts.map +1 -0
  145. package/dist/utils/story-markdown.js +82 -0
  146. package/dist/utils/story-markdown.js.map +1 -0
  147. package/dist/utils/story-markdown.test.d.ts +2 -0
  148. package/dist/utils/story-markdown.test.d.ts.map +1 -0
  149. package/dist/utils/story-markdown.test.js +143 -0
  150. package/dist/utils/story-markdown.test.js.map +1 -0
  151. package/package.json +1 -1
  152. package/src/agents/base-agent.ts +5 -0
  153. package/src/agents/intermediate.ts +2 -2
  154. package/src/agents/junior.ts +2 -2
  155. package/src/agents/qa.ts +13 -8
  156. package/src/agents/senior.ts +21 -11
  157. package/src/agents/tech-lead.ts +28 -13
  158. package/src/cli/commands/assign.test.ts +5 -0
  159. package/src/cli/commands/assign.ts +4 -2
  160. package/src/cli/commands/manager/handoff-recovery.ts +4 -2
  161. package/src/cli/commands/manager/index.ts +16 -11
  162. package/src/cli/commands/manager/tech-lead-lifecycle.ts +9 -3
  163. package/src/cli/commands/msg.ts +8 -7
  164. package/src/cli/commands/my-stories.ts +22 -13
  165. package/src/cli/commands/nuke.test.ts +31 -0
  166. package/src/cli/commands/nuke.ts +18 -7
  167. package/src/cli/commands/pr.test.ts +77 -1
  168. package/src/cli/commands/pr.ts +5 -0
  169. package/src/cli/commands/req.ts +13 -6
  170. package/src/cli/commands/resume.ts +4 -1
  171. package/src/cli/commands/stories.ts +22 -13
  172. package/src/cli/dashboard/panels/agents.ts +7 -3
  173. package/src/cli-runtimes/chrome.ts +43 -0
  174. package/src/cli-runtimes/claude.ts +26 -9
  175. package/src/cli-runtimes/codex.ts +12 -3
  176. package/src/cli-runtimes/gemini.ts +12 -3
  177. package/src/cli-runtimes/index.test.ts +158 -0
  178. package/src/cli-runtimes/index.ts +3 -2
  179. package/src/cli-runtimes/types.ts +19 -2
  180. package/src/config/schema.ts +6 -0
  181. package/src/context-files/generator.ts +3 -2
  182. package/src/context-files/index.test.ts +2 -0
  183. package/src/db/client.ts +7 -0
  184. package/src/db/migrations/015-add-story-markdown-path.sql +5 -0
  185. package/src/db/queries/stories.ts +29 -5
  186. package/src/db/queries/test-helpers.ts +1 -0
  187. package/src/git/worktree.test.ts +43 -0
  188. package/src/git/worktree.ts +10 -0
  189. package/src/orchestrator/prompt-templates.test.ts +4 -0
  190. package/src/orchestrator/prompt-templates.ts +20 -8
  191. package/src/orchestrator/scheduler.test.ts +1 -0
  192. package/src/orchestrator/scheduler.ts +33 -12
  193. package/src/tmux/manager.ts +42 -13
  194. package/src/utils/auto-merge.test.ts +81 -0
  195. package/src/utils/auto-merge.ts +78 -5
  196. package/src/utils/instance.test.ts +129 -0
  197. package/src/utils/instance.ts +95 -0
  198. package/src/utils/paths.test.ts +8 -0
  199. package/src/utils/paths.ts +3 -0
  200. package/src/utils/story-markdown.test.ts +176 -0
  201. package/src/utils/story-markdown.ts +94 -0
@@ -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',
@@ -15,6 +15,7 @@ import { createLog } from '../../db/queries/logs.js';
15
15
  import { createRequirement, updateRequirement } from '../../db/queries/requirements.js';
16
16
  import { getAllTeams } from '../../db/queries/teams.js';
17
17
  import { isTmuxAvailable, spawnTmuxSession } from '../../tmux/manager.js';
18
+ import { getTechLeadSessionName } from '../../utils/instance.js';
18
19
  import { withHiveContext } from '../../utils/with-hive-context.js';
19
20
  import { startDashboard } from '../dashboard/index.js';
20
21
 
@@ -209,21 +210,25 @@ export const reqCommand = new Command('req')
209
210
  });
210
211
 
211
212
  // Spawn Tech Lead tmux session
212
- const sessionName = `hive-tech-lead`;
213
+ const sessionName = getTechLeadSessionName(paths.hiveDir);
213
214
  const techLeadPrompt = generateTechLeadPrompt(
214
215
  req.id,
215
216
  title,
216
217
  description,
217
218
  teams,
218
219
  options.godmode,
219
- targetBranch
220
+ targetBranch,
221
+ sessionName
220
222
  );
221
223
 
222
224
  try {
223
225
  // Build CLI command using the configured runtime for Tech Lead
226
+ const chromeEnabled =
227
+ config.agents?.chrome_enabled === true && techLeadCliTool === 'claude';
224
228
  const commandArgs = getCliRuntimeBuilder(techLeadCliTool).buildSpawnCommand(
225
229
  techLeadModel,
226
- techLeadSafetyMode
230
+ techLeadSafetyMode,
231
+ { chrome: chromeEnabled }
227
232
  );
228
233
 
229
234
  // Pass the prompt as initialPrompt so it's included as a CLI positional
@@ -318,8 +323,10 @@ export function generateTechLeadPrompt(
318
323
  description: string,
319
324
  teams: { id: string; name: string; repo_path: string; repo_url: string }[],
320
325
  godmode?: boolean,
321
- targetBranch?: string
326
+ targetBranch?: string,
327
+ techLeadSession?: string
322
328
  ): string {
329
+ const tlSession = techLeadSession || 'hive-tech-lead';
323
330
  const teamList = teams.map(t => `- ${t.name}: ${t.repo_path} (${t.repo_url})`).join('\n');
324
331
  const godmodeNotice = godmode
325
332
  ? `
@@ -384,7 +391,7 @@ The SQLite database is at .hive/hive.db
384
391
 
385
392
  Check your inbox for messages from developers:
386
393
  \`\`\`bash
387
- hive msg inbox hive-tech-lead
394
+ hive msg inbox ${tlSession}
388
395
  \`\`\`
389
396
 
390
397
  Read a specific message:
@@ -397,7 +404,7 @@ Reply to a message:
397
404
  hive msg reply <msg-id> "Your response here"
398
405
  \`\`\`
399
406
 
400
- **IMPORTANT:** Periodically run \`hive msg inbox hive-tech-lead\` to check if any developers need guidance. Answer their questions promptly to keep the team unblocked.
407
+ **IMPORTANT:** Periodically run \`hive msg inbox ${tlSession}\` to check if any developers need guidance. Answer their questions promptly to keep the team unblocked.
401
408
 
402
409
  When done planning, update the requirement status to 'planned' and run \`hive assign\` to spawn Senior developers who will implement the stories.
403
410
  `;
@@ -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({
@@ -87,23 +87,32 @@ storiesCommand
87
87
  criteria?: string[];
88
88
  json?: boolean;
89
89
  }) => {
90
- await withHiveContext(async ({ root, db }) => {
90
+ await withHiveContext(async ({ root, paths, db }) => {
91
91
  // Create local story
92
- const story = createStory(db.db, {
93
- requirementId: options.requirement || null,
94
- teamId: options.team || null,
95
- title: options.title,
96
- description: options.description,
97
- acceptanceCriteria: options.criteria || null,
98
- });
92
+ const story = createStory(
93
+ db.db,
94
+ {
95
+ requirementId: options.requirement || null,
96
+ teamId: options.team || null,
97
+ title: options.title,
98
+ description: options.description,
99
+ acceptanceCriteria: options.criteria || null,
100
+ },
101
+ paths.storiesDir
102
+ );
99
103
 
100
104
  // Update with optional fields
101
105
  if (options.points !== undefined || options.complexity !== undefined) {
102
- updateStory(db.db, story.id, {
103
- storyPoints: options.points ?? null,
104
- complexityScore: options.complexity ?? null,
105
- status: 'estimated',
106
- });
106
+ updateStory(
107
+ db.db,
108
+ story.id,
109
+ {
110
+ storyPoints: options.points ?? null,
111
+ complexityScore: options.complexity ?? null,
112
+ status: 'estimated',
113
+ },
114
+ paths.storiesDir
115
+ );
107
116
  }
108
117
 
109
118
  // Sync to PM provider if configured
@@ -9,6 +9,7 @@ import type { ModelsConfig } from '../../../config/schema.js';
9
9
  import { getActiveAgents, type AgentRow } from '../../../db/queries/agents.js';
10
10
  import { getTeamById } from '../../../db/queries/teams.js';
11
11
  import { getHiveSessions } from '../../../tmux/manager.js';
12
+ import { getManagerSessionName } from '../../../utils/instance.js';
12
13
  import { findHiveRoot, getHivePaths } from '../../../utils/paths.js';
13
14
 
14
15
  function debugLog(msg: string) {
@@ -125,8 +126,11 @@ export async function updateAgentsPanel(list: Widgets.ListElement, db: Database)
125
126
  }
126
127
 
127
128
  // Check for manager session (not in DB)
128
- const hiveSessions = await getHiveSessions();
129
- const managerSession = hiveSessions.find(s => s.name === 'hive-manager');
129
+ const hiveRoot = findHiveRoot();
130
+ const hDir = hiveRoot ? getHivePaths(hiveRoot).hiveDir : undefined;
131
+ const managerName = hDir ? getManagerSessionName(hDir) : 'hive-manager';
132
+ const hiveSessions = await getHiveSessions(hDir);
133
+ const managerSession = hiveSessions.find(s => s.name === managerName);
130
134
 
131
135
  // Build combined list - manager first if running
132
136
  const displayAgents: DisplayAgent[] = [];
@@ -137,7 +141,7 @@ export async function updateAgentsPanel(list: Widgets.ListElement, db: Database)
137
141
  id: 'manager',
138
142
  type: 'manager' as AgentRow['type'],
139
143
  team_id: null,
140
- tmux_session: 'hive-manager',
144
+ tmux_session: managerName,
141
145
  model: '-',
142
146
  status: 'working',
143
147
  current_story_id: null,
@@ -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: