@link-assistant/hive-mind 1.62.1 → 1.64.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/src/solve.mjs CHANGED
@@ -89,10 +89,18 @@ setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log f
89
89
 
90
90
  // Early logs go to cwd; custom log dir takes effect after argv is parsed
91
91
  // Conditionally import tool-specific functions after argv is parsed
92
+ // If --use-agent-commander is enabled, use agent-commander's checkForUncommittedChanges
92
93
  let checkForUncommittedChanges;
93
- if (argv.tool === 'opencode') {
94
+ let agentCommanderLib = null;
95
+ if (argv.useAgentCommander) {
96
+ agentCommanderLib = await import('./agent-commander.lib.mjs');
97
+ checkForUncommittedChanges = agentCommanderLib.checkForUncommittedChanges;
98
+ } else if (argv.tool === 'opencode') {
94
99
  const opencodeLib = await import('./opencode.lib.mjs');
95
100
  checkForUncommittedChanges = opencodeLib.checkForUncommittedChanges;
101
+ } else if (argv.tool === 'gemini') {
102
+ const geminiLib = await import('./gemini.lib.mjs');
103
+ checkForUncommittedChanges = geminiLib.checkForUncommittedChanges;
96
104
  } else if (argv.tool === 'codex') {
97
105
  const codexLib = await import('./codex.lib.mjs');
98
106
  checkForUncommittedChanges = codexLib.checkForUncommittedChanges;
@@ -664,73 +672,26 @@ try {
664
672
 
665
673
  // Execute tool command with all prompts and settings
666
674
  let toolResult;
667
- if (argv.tool === 'opencode') {
668
- const opencodeLib = await import('./opencode.lib.mjs');
669
- const { executeOpenCode, checkPlaywrightMcpAvailability: checkOpenCodePlaywrightMcp } = opencodeLib;
670
- const opencodePath = process.env.OPENCODE_PATH || 'opencode';
671
- await resolvePlaywrightMcp(checkOpenCodePlaywrightMcp);
672
675
 
673
- toolResult = await executeOpenCode({
674
- issueUrl,
675
- issueNumber,
676
- prNumber,
677
- prUrl,
678
- branchName,
679
- tempDir,
680
- workspaceTmpDir,
681
- isContinueMode,
682
- mergeStateStatus,
683
- forkedRepo,
684
- feedbackLines,
685
- forkActionsUrl,
686
- owner,
687
- repo,
688
- argv,
689
- log,
690
- setLogFile,
691
- getLogFile,
692
- formatAligned,
693
- getResourceSnapshot,
694
- opencodePath,
695
- $,
696
- });
697
- } else if (argv.tool === 'codex') {
698
- const codexLib = await import('./codex.lib.mjs');
699
- const { executeCodex, checkPlaywrightMcpAvailability } = codexLib;
700
- const codexPath = process.env.CODEX_PATH || 'codex';
701
- await resolvePlaywrightMcp(checkPlaywrightMcpAvailability);
676
+ // If --use-agent-commander is enabled, use agent-commander for all tools
677
+ if (argv.useAgentCommander) {
678
+ // Ensure agent-commander is available
679
+ if (!agentCommanderLib) {
680
+ agentCommanderLib = await import('./agent-commander.lib.mjs');
681
+ }
702
682
 
703
- toolResult = await executeCodex({
704
- issueUrl,
705
- issueNumber,
706
- prNumber,
707
- prUrl,
708
- branchName,
709
- tempDir,
710
- workspaceTmpDir,
711
- isContinueMode,
712
- mergeStateStatus,
713
- forkedRepo,
714
- feedbackLines,
715
- forkActionsUrl,
716
- owner,
717
- repo,
718
- argv,
719
- log,
720
- setLogFile,
721
- getLogFile,
722
- formatAligned,
723
- getResourceSnapshot,
724
- codexPath,
725
- $,
726
- });
727
- } else if (argv.tool === 'agent') {
728
- const agentLib = await import('./agent.lib.mjs');
729
- const { executeAgent, checkPlaywrightMcpAvailability: checkAgentPlaywrightMcp } = agentLib;
730
- const agentPath = process.env.AGENT_PATH || 'agent';
731
- await resolvePlaywrightMcp(checkAgentPlaywrightMcp);
683
+ const isAvailable = await agentCommanderLib.isAgentCommanderAvailable();
684
+ if (!isAvailable) {
685
+ await log('\n[agent-commander] agent-commander is not installed.', { level: 'error' });
686
+ await log(' Install it with: npm install agent-commander', { level: 'error' });
687
+ await log(' Or remove the --use-agent-commander flag to use embedded tool logic.', { level: 'error' });
688
+ await safeExit(1, 'agent-commander not available');
689
+ }
690
+
691
+ await log(`\n[agent-commander] Using agent-commander for ${argv.tool || 'claude'} execution`);
692
+ await agentCommanderLib.resolvePlaywrightMcpForAgentCommander({ argv, log, tool: argv.tool || 'claude' });
732
693
 
733
- toolResult = await executeAgent({
694
+ toolResult = await agentCommanderLib.executeWithAgentCommander({
734
695
  issueUrl,
735
696
  issueNumber,
736
697
  prNumber,
@@ -751,16 +712,20 @@ try {
751
712
  getLogFile,
752
713
  formatAligned,
753
714
  getResourceSnapshot,
754
- agentPath,
755
715
  $,
756
716
  });
757
- } else if (argv.tool === 'qwen') {
758
- const qwenLib = await import('./qwen.lib.mjs');
759
- const { executeQwen, checkPlaywrightMcpAvailability: checkQwenPlaywrightMcp } = qwenLib;
760
- const qwenPath = process.env.QWEN_PATH || 'qwen';
761
- await resolvePlaywrightMcp(checkQwenPlaywrightMcp);
762
-
763
- toolResult = await executeQwen({
717
+ } else if (['opencode', 'codex', 'agent', 'gemini', 'qwen'].includes(argv.tool)) {
718
+ const toolDispatch = {
719
+ opencode: { lib: './opencode.lib.mjs', execFn: 'executeOpenCode', envVar: 'OPENCODE_PATH', defaultBin: 'opencode', pathKey: 'opencodePath' },
720
+ codex: { lib: './codex.lib.mjs', execFn: 'executeCodex', envVar: 'CODEX_PATH', defaultBin: 'codex', pathKey: 'codexPath' },
721
+ agent: { lib: './agent.lib.mjs', execFn: 'executeAgent', envVar: 'AGENT_PATH', defaultBin: 'agent', pathKey: 'agentPath' },
722
+ gemini: { lib: './gemini.lib.mjs', execFn: 'executeGemini', envVar: 'GEMINI_PATH', defaultBin: 'gemini', pathKey: 'geminiPath' },
723
+ qwen: { lib: './qwen.lib.mjs', execFn: 'executeQwen', envVar: 'QWEN_PATH', defaultBin: 'qwen', pathKey: 'qwenPath' },
724
+ }[argv.tool];
725
+ const toolLib = await import(toolDispatch.lib);
726
+ await resolvePlaywrightMcp(toolLib.checkPlaywrightMcpAvailability);
727
+
728
+ toolResult = await toolLib[toolDispatch.execFn]({
764
729
  issueUrl,
765
730
  issueNumber,
766
731
  prNumber,
@@ -781,7 +746,7 @@ try {
781
746
  getLogFile,
782
747
  formatAligned,
783
748
  getResourceSnapshot,
784
- qwenPath,
749
+ [toolDispatch.pathKey]: process.env[toolDispatch.envVar] || toolDispatch.defaultBin,
785
750
  $,
786
751
  });
787
752
  } else {
@@ -167,7 +167,7 @@ export const getUncommittedChangesDetails = async tempDir => {
167
167
  };
168
168
 
169
169
  /**
170
- * Execute the AI tool (Claude, OpenCode, Codex, Agent, Qwen) for a restart iteration
170
+ * Execute the AI tool (Claude, OpenCode, Codex, Agent, Gemini, Qwen) for a restart iteration
171
171
  * This is the shared tool execution logic used by both watch mode and auto-restart-until-mergeable mode
172
172
  * @param {Object} params - Execution parameters
173
173
  * @returns {Promise<Object>} - Tool execution result
@@ -183,7 +183,34 @@ export const executeToolIteration = async params => {
183
183
  await cascadePlaywrightMcpDisable(argv, log);
184
184
 
185
185
  let toolResult;
186
- if (argv.tool === 'opencode') {
186
+ if (argv.useAgentCommander) {
187
+ const agentCommanderLib = await import('./agent-commander.lib.mjs');
188
+ await agentCommanderLib.resolvePlaywrightMcpForAgentCommander({ argv, log, tool: argv.tool || 'claude' });
189
+
190
+ toolResult = await agentCommanderLib.executeWithAgentCommander({
191
+ issueUrl,
192
+ issueNumber,
193
+ prNumber,
194
+ prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
195
+ branchName,
196
+ tempDir,
197
+ workspaceTmpDir: params.workspaceTmpDir,
198
+ isContinueMode: true,
199
+ mergeStateStatus,
200
+ forkedRepo: argv.fork,
201
+ feedbackLines,
202
+ forkActionsUrl: null,
203
+ owner,
204
+ repo,
205
+ argv,
206
+ log,
207
+ formatAligned,
208
+ getResourceSnapshot,
209
+ setLogFile: () => {},
210
+ getLogFile: () => '',
211
+ $,
212
+ });
213
+ } else if (argv.tool === 'opencode') {
187
214
  // Use OpenCode
188
215
  const opencodeExecLib = await import('./opencode.lib.mjs');
189
216
  const { executeOpenCode, checkPlaywrightMcpAvailability } = opencodeExecLib;
@@ -208,6 +235,7 @@ export const executeToolIteration = async params => {
208
235
  prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
209
236
  branchName,
210
237
  tempDir,
238
+ workspaceTmpDir,
211
239
  isContinueMode: true,
212
240
  mergeStateStatus,
213
241
  forkedRepo: argv.fork,
@@ -246,6 +274,7 @@ export const executeToolIteration = async params => {
246
274
  prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
247
275
  branchName,
248
276
  tempDir,
277
+ workspaceTmpDir,
249
278
  isContinueMode: true,
250
279
  mergeStateStatus,
251
280
  forkedRepo: argv.fork,
@@ -287,6 +316,7 @@ export const executeToolIteration = async params => {
287
316
  prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
288
317
  branchName,
289
318
  tempDir,
319
+ workspaceTmpDir,
290
320
  isContinueMode: true,
291
321
  mergeStateStatus,
292
322
  forkedRepo: argv.fork,
@@ -301,6 +331,48 @@ export const executeToolIteration = async params => {
301
331
  agentPath,
302
332
  $,
303
333
  });
334
+ } else if (argv.tool === 'gemini') {
335
+ // Use Gemini
336
+ const geminiExecLib = await import('./gemini.lib.mjs');
337
+ const { executeGemini, checkPlaywrightMcpAvailability } = geminiExecLib;
338
+ const geminiPath = argv.geminiPath || 'gemini';
339
+
340
+ if (argv.promptPlaywrightMcp) {
341
+ const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
342
+ if (playwrightMcpAvailable) {
343
+ await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
344
+ } else {
345
+ await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
346
+ argv.promptPlaywrightMcp = false;
347
+ }
348
+ } else {
349
+ await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
350
+ }
351
+
352
+ toolResult = await executeGemini({
353
+ issueUrl,
354
+ issueNumber,
355
+ prNumber,
356
+ prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
357
+ branchName,
358
+ tempDir,
359
+ workspaceTmpDir,
360
+ isContinueMode: true,
361
+ mergeStateStatus,
362
+ forkedRepo: argv.fork,
363
+ feedbackLines,
364
+ forkActionsUrl: null,
365
+ owner,
366
+ repo,
367
+ argv,
368
+ log,
369
+ setLogFile: () => {},
370
+ getLogFile: () => '',
371
+ formatAligned,
372
+ getResourceSnapshot,
373
+ geminiPath,
374
+ $,
375
+ });
304
376
  } else if (argv.tool === 'qwen') {
305
377
  // Use Qwen Code
306
378
  const qwenExecLib = await import('./qwen.lib.mjs');
@@ -371,6 +443,7 @@ export const executeToolIteration = async params => {
371
443
  prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
372
444
  branchName,
373
445
  tempDir,
446
+ workspaceTmpDir,
374
447
  isContinueMode: true,
375
448
  mergeStateStatus,
376
449
  forkedRepo: argv.fork,
@@ -46,7 +46,7 @@ export const { buildClaudeResumeCommand, buildClaudeInitialCommand } = claudeCom
46
46
  * @param {Object} options
47
47
  * @param {string} options.issueUrl - The issue URL passed to solve.mjs
48
48
  * @param {string} options.sessionId - The session ID to resume
49
- * @param {string|null} [options.tool] - Tool name (codex, opencode, agent)
49
+ * @param {string|null} [options.tool] - Tool name (codex, opencode, agent, gemini)
50
50
  * @param {string|null} [options.model] - Model name to preserve
51
51
  * @param {string|null} [options.fallbackModel] - Explicit fallback model to preserve
52
52
  * @param {string|null} [options.tempDir] - Working directory to preserve
@@ -295,7 +295,18 @@ export const performSystemChecks = async (minDiskSpace = 2048, skipToolConnectio
295
295
  // Skip tool connection validation if in dry-run mode or explicitly requested
296
296
  if (!skipToolConnection) {
297
297
  let isToolConnected = false;
298
- if (argv.tool === 'opencode') {
298
+ if (argv.useAgentCommander) {
299
+ const agentCommanderLib = await import('./agent-commander.lib.mjs');
300
+ isToolConnected = await agentCommanderLib.validateAgentCommanderConnection({
301
+ tool: argv.tool || 'claude',
302
+ model,
303
+ log,
304
+ });
305
+ if (!isToolConnected) {
306
+ await log('❌ Cannot proceed without agent-commander tool connection', { level: 'error' });
307
+ return false;
308
+ }
309
+ } else if (argv.tool === 'opencode') {
299
310
  // Validate OpenCode connection
300
311
  const opencodeLib = await import('./opencode.lib.mjs');
301
312
  isToolConnected = await opencodeLib.validateOpenCodeConnection(model);
@@ -303,6 +314,14 @@ export const performSystemChecks = async (minDiskSpace = 2048, skipToolConnectio
303
314
  await log('❌ Cannot proceed without OpenCode connection', { level: 'error' });
304
315
  return false;
305
316
  }
317
+ } else if (argv.tool === 'gemini') {
318
+ // Validate Gemini connection
319
+ const geminiLib = await import('./gemini.lib.mjs');
320
+ isToolConnected = await geminiLib.validateGeminiConnection(model);
321
+ if (!isToolConnected) {
322
+ await log('❌ Cannot proceed without Gemini CLI connection', { level: 'error' });
323
+ return false;
324
+ }
306
325
  } else if (argv.tool === 'codex') {
307
326
  // Validate Codex connection
308
327
  const codexLib = await import('./codex.lib.mjs');
@@ -1,7 +1,7 @@
1
1
  import { buildModelOptionDescription, defaultModels } from './models/index.mjs';
2
2
  import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
3
3
 
4
- export const TASK_TOOL_CHOICES = ['claude', 'codex', 'opencode', 'agent'];
4
+ export const TASK_TOOL_CHOICES = ['claude', 'codex', 'opencode', 'agent', 'gemini', 'qwen'];
5
5
 
6
6
  export function getDefaultTaskModel(tool) {
7
7
  return defaultModels[tool] || defaultModels.claude;
package/src/task.mjs CHANGED
@@ -33,7 +33,7 @@ if (earlyArgs.length === 0 || earlyArgs.includes('--help') || earlyArgs.includes
33
33
  console.log(' --only-decompose Only run decomposition mode');
34
34
  console.log(' --split Split a GitHub issue into smaller issues');
35
35
  console.log(' --split-count Number of issues to split into [default: 2]');
36
- console.log(' --tool AI tool for agent-commander read-only mode (claude, codex, opencode, agent, qwen) [default: claude]');
36
+ console.log(' --tool AI tool for agent-commander read-only mode (claude, codex, opencode, agent, qwen, gemini) [default: claude]');
37
37
  console.log(' --model, -m Model to use');
38
38
  console.log(' --isolation agent-commander isolation mode [default: screen]');
39
39
  console.log(' --dry-run Print split output without creating GitHub issues');
@@ -342,7 +342,7 @@ function isForwardedOrReply(ctx) {
342
342
  /**
343
343
  * Validates the model name in the args array and returns an error message if invalid
344
344
  * @param {string[]} args - Array of command arguments
345
- * @param {string} tool - The tool to validate against ('claude' or 'opencode')
345
+ * @param {string} tool - The tool to validate against ('claude', 'opencode', 'codex', 'agent', or 'gemini')
346
346
  * @returns {string|null} Error message if invalid, null if valid or no model specified
347
347
  */
348
348
  function validateModelInArgs(args, tool = 'claude') {
@@ -525,7 +525,7 @@ bot.command('help', async ctx => {
525
525
  message += '📝 *Available Commands:*\n\n';
526
526
 
527
527
  if (solveEnabled) {
528
- message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */qwen*) - Solve a GitHub issue\n';
528
+ message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - Solve a GitHub issue\n';
529
529
  message += 'Usage: `/solve <github-url> [options]`\n';
530
530
  message += 'Example: `/solve https://github.com/owner/repo/issues/123 --model sonnet`\n';
531
531
  message += 'Tool aliases imply `--tool <tool>`: `/codex <github-url>` equals `/solve <github-url> --tool codex`\n';
@@ -535,7 +535,7 @@ bot.command('help', async ctx => {
535
535
  }
536
536
  message += '\n';
537
537
  } else {
538
- message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */qwen*) - ❌ Disabled\n\n';
538
+ message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ Disabled\n\n';
539
539
  }
540
540
 
541
541
  if (taskEnabled) {
@@ -576,7 +576,7 @@ bot.command('help', async ctx => {
576
576
  message += '🔔 *Session Notifications:* Completion notifications are automatic; use /subscribe for private DM forwards.\n';
577
577
  if (ISOLATION_BACKEND) message += `🔒 *Isolation Mode:* \`${ISOLATION_BACKEND}\` (experimental)\n`;
578
578
  message += '\n';
579
- message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /qwen, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /subscribe and /unsubscribe work in private and group chats.\n\n';
579
+ message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /subscribe and /unsubscribe work in private and group chats.\n\n';
580
580
  message += '🔧 *Common Options:*\n';
581
581
  message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
582
582
  message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
@@ -14,6 +14,7 @@ export const TOOL_SOLVE_COMMAND_ALIASES = Object.freeze({
14
14
  opencode: 'opencode',
15
15
  agent: 'agent',
16
16
  qwen: 'qwen',
17
+ gemini: 'gemini',
17
18
  });
18
19
 
19
20
  export const SOLVE_COMMAND_NAMES = Object.freeze(['solve', 'do', 'continue', ...Object.keys(TOOL_SOLVE_COMMAND_ALIASES)]);
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Features:
8
8
  * - Shows pending, processing, completed, and failed queue items
9
- * - Per-tool queue breakdown (claude, opencode, etc.)
9
+ * - Per-tool queue breakdown (claude, opencode, codex, agent, gemini, etc.)
10
10
  * - Lists currently processing and waiting items
11
11
  * - Running Claude process count
12
12
  *
@@ -7,7 +7,7 @@ const execAsync = promisify(exec);
7
7
 
8
8
  /**
9
9
  * Count running processes by name.
10
- * @param {string} processName - Process name to search for (e.g., 'claude', 'agent')
10
+ * @param {string} processName - Process name to search for (e.g., 'claude', 'agent', 'codex', 'gemini')
11
11
  * @param {boolean} verbose - Whether to log verbose output
12
12
  * @returns {Promise<{count: number, processes: string[]}>}
13
13
  */
@@ -84,6 +84,15 @@ export async function getRunningQwenProcesses(verbose = false) {
84
84
  return getRunningProcesses('qwen', verbose);
85
85
  }
86
86
 
87
+ /**
88
+ * Count running gemini processes.
89
+ * @param {boolean} verbose - Whether to log verbose output
90
+ * @returns {Promise<{count: number, processes: string[]}>}
91
+ */
92
+ export async function getRunningGeminiProcesses(verbose = false) {
93
+ return getRunningProcesses('gemini', verbose);
94
+ }
95
+
87
96
  /**
88
97
  * Format a threshold as percentage for display.
89
98
  * @param {number} ratio - Ratio (0.0 - 1.0)
@@ -156,6 +165,8 @@ export function formatWaitingReason(metric, currentValue, threshold) {
156
165
  return 'Codex process is already running';
157
166
  case 'qwen_running':
158
167
  return 'Qwen Code process is already running';
168
+ case 'gemini_running':
169
+ return 'Gemini CLI process is already running';
159
170
  default:
160
171
  return `${metric} threshold exceeded`;
161
172
  }
@@ -16,8 +16,8 @@
16
16
  */
17
17
 
18
18
  import { getCachedClaudeLimits, getCachedCodexLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
19
- export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
20
- import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
19
+ export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
20
+ import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
21
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
23
  import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
@@ -128,7 +128,7 @@ class SolveQueueItem {
128
128
  * Solve Queue - Producer/Consumer queue for /solve commands
129
129
  *
130
130
  * Uses separate queues for each tool type to ensure:
131
- * - Claude tasks never block agent tasks (and vice versa)
131
+ * - Claude tasks never block agent, codex, gemini, or qwen tasks (and vice versa)
132
132
  * - Each tool queue maintains FIFO order
133
133
  * - Each tool has independent rate limiting
134
134
  *
@@ -143,13 +143,14 @@ export class SolveQueue {
143
143
  this.getRunningIsolatedSessionsFn = options.getRunningIsolatedSessions || getRunningIsolatedSessions;
144
144
  this.autoStart = options.autoStart !== false;
145
145
 
146
- // Separate queues per tool type - claude tasks never block agent tasks
146
+ // Separate queues per tool type - claude tasks never block other tool tasks
147
147
  // See: https://github.com/link-assistant/hive-mind/issues/1159
148
148
  this.queues = {
149
149
  claude: [],
150
150
  agent: [],
151
151
  codex: [],
152
152
  qwen: [],
153
+ gemini: [],
153
154
  };
154
155
  this.processing = new Map();
155
156
  this.completed = [];
@@ -162,6 +163,7 @@ export class SolveQueue {
162
163
  agent: null,
163
164
  codex: null,
164
165
  qwen: null,
166
+ gemini: null,
165
167
  };
166
168
  // Legacy: keep for compatibility with existing code that uses lastStartTime
167
169
  this.lastStartTime = null;
@@ -184,7 +186,7 @@ export class SolveQueue {
184
186
 
185
187
  /**
186
188
  * Get the queue array for a specific tool, creating it if needed
187
- * @param {string} tool - Tool type ('claude', 'agent', etc.)
189
+ * @param {string} tool - Tool type ('claude', 'agent', 'codex', 'gemini', etc.)
188
190
  * @returns {Array} The queue array for this tool
189
191
  */
190
192
  getToolQueue(tool) {
@@ -333,7 +335,7 @@ export class SolveQueue {
333
335
  /**
334
336
  * Count processing items by tool type
335
337
  * Used for tool-specific limit checking - e.g., Claude limits only count Claude processing items
336
- * @param {string} tool - Tool type to count ('claude', 'agent', etc.)
338
+ * @param {string} tool - Tool type to count ('claude', 'agent', 'codex', 'gemini', etc.)
337
339
  * @returns {number} Count of processing items with the specified tool
338
340
  * @see https://github.com/link-assistant/hive-mind/issues/1159
339
341
  */
@@ -378,6 +380,7 @@ export class SolveQueue {
378
380
 
379
381
  if (check.canStart) {
380
382
  const item = toolQueue[0];
383
+ if (!item) continue;
381
384
  // Also check one-at-a-time mode for this specific tool
382
385
  // For tool-specific one-at-a-time, only count that tool's processing items
383
386
  const toolProcessingCount = this.getProcessingCountByTool(tool);
@@ -397,7 +400,7 @@ export class SolveQueue {
397
400
  * Reject all items in a tool queue and notify users.
398
401
  * Called when a 'reject' strategy threshold is exceeded for queued items.
399
402
  *
400
- * @param {string} tool - Tool type (e.g., 'claude', 'agent')
403
+ * @param {string} tool - Tool type (e.g., 'claude', 'agent', 'codex', 'gemini')
401
404
  * @param {SolveQueueItem[]} toolQueue - The tool's queue array
402
405
  * @param {string} rejectReason - Reason for rejection
403
406
  * @see https://github.com/link-assistant/hive-mind/issues/1555
@@ -523,8 +526,8 @@ export class SolveQueue {
523
526
  *
524
527
  * Logic per issue #1159:
525
528
  * - Different tools have different limits. Claude limits only apply to 'claude' tool.
526
- * - Processing count for Claude limits only includes Claude items, not agent items.
527
- * - This allows agent tasks to run in parallel when Claude limits are reached.
529
+ * - Processing count for Claude limits only includes Claude items, not agent/codex/gemini/qwen items.
530
+ * - This allows non-Claude tasks to run in parallel when Claude limits are reached.
528
531
  *
529
532
  * Logic per issue #1253:
530
533
  * - All thresholds now support configurable strategies (reject, enqueue, dequeue-one-at-a-time)
@@ -533,7 +536,7 @@ export class SolveQueue {
533
536
  * - 'dequeue-one-at-a-time' allows one command while blocking subsequent
534
537
  *
535
538
  * @param {Object} options - Options for the check
536
- * @param {string} options.tool - The tool being used ('claude', 'agent', etc.)
539
+ * @param {string} options.tool - The tool being used ('claude', 'agent', 'codex', 'gemini', 'qwen', etc.)
537
540
  * @returns {Promise<{canStart: boolean, rejected?: boolean, rejectReason?: string, reason?: string, reasons?: string[], oneAtATime?: boolean}>}
538
541
  */
539
542
  async canStartCommand(options = {}) {
@@ -564,9 +567,11 @@ export class SolveQueue {
564
567
  const codexProcessCount = externalProcessing.byTool.codex || 0;
565
568
  const agentProcessCount = externalProcessing.byTool.agent || 0;
566
569
  const qwenProcessCount = externalProcessing.byTool.qwen || 0;
570
+ const geminiProcessCount = externalProcessing.byTool.gemini || 0;
567
571
  const hasRunningClaude = claudeProcessCount > 0;
568
572
  const hasRunningCodex = codexProcessCount > 0;
569
573
  const hasRunningQwen = qwenProcessCount > 0;
574
+ const hasRunningGemini = geminiProcessCount > 0;
570
575
 
571
576
  // Calculate total processing count for system resources (all tools)
572
577
  // System resources (RAM, CPU, disk) apply to all tools
@@ -574,11 +579,12 @@ export class SolveQueue {
574
579
 
575
580
  // Calculate Claude-specific processing count for Claude API limits
576
581
  // Only counts Claude items in queue + external claude processes
577
- // Agent items don't count against Claude's one-at-a-time limit
582
+ // Non-Claude items don't count against Claude's one-at-a-time limit
578
583
  // See: https://github.com/link-assistant/hive-mind/issues/1159
579
584
  const claudeProcessingCount = this.getProcessingCountByTool('claude');
580
585
  const codexProcessingCount = this.getProcessingCountByTool('codex');
581
586
  const qwenProcessingCount = this.getProcessingCountByTool('qwen');
587
+ const geminiProcessingCount = this.getProcessingCountByTool('gemini');
582
588
 
583
589
  // Track claude_running as a metric (but don't add to reasons yet)
584
590
  if (hasRunningClaude) {
@@ -590,6 +596,9 @@ export class SolveQueue {
590
596
  if (hasRunningQwen) {
591
597
  this.recordThrottle('qwen_running');
592
598
  }
599
+ if (hasRunningGemini) {
600
+ this.recordThrottle('gemini_running');
601
+ }
593
602
 
594
603
  // Check system resources with strategy support
595
604
  // System resources apply to ALL tools, not just Claude
@@ -609,11 +618,11 @@ export class SolveQueue {
609
618
 
610
619
  // Check API limits with strategy support (pass hasRunningClaude, claudeProcessingCount, and tool)
611
620
  // Claude limits use claudeProcessingCount (only Claude items), not totalProcessing
612
- // This allows agent tasks to proceed when Claude limits are reached
621
+ // This allows non-Claude tasks to proceed when Claude limits are reached
613
622
  // See: https://github.com/link-assistant/hive-mind/issues/1159
614
623
  // See: https://github.com/link-assistant/hive-mind/issues/1253 (strategies)
615
- const hasRunningToolProcess = tool === 'codex' ? hasRunningCodex : tool === 'qwen' ? hasRunningQwen : hasRunningClaude;
616
- const toolProcessingCount = tool === 'codex' ? codexProcessingCount : tool === 'qwen' ? qwenProcessingCount : claudeProcessingCount;
624
+ const hasRunningToolProcess = (externalProcessing.byTool[tool] || 0) > 0;
625
+ const toolProcessingCount = this.getProcessingCountByTool(tool);
617
626
  const limitCheck = await this.checkApiLimits(hasRunningToolProcess, toolProcessingCount, tool);
618
627
  if (limitCheck.rejected) {
619
628
  rejected = true;
@@ -640,6 +649,9 @@ export class SolveQueue {
640
649
  if (tool === 'qwen' && hasRunningQwen && reasons.length > 0) {
641
650
  reasons.push(formatWaitingReason('qwen_running', qwenProcessCount, 0) + ` (${qwenProcessCount} processes)`);
642
651
  }
652
+ if (tool === 'gemini' && hasRunningGemini && reasons.length > 0) {
653
+ reasons.push(formatWaitingReason('gemini_running', geminiProcessCount, 0) + ` (${geminiProcessCount} processes)`);
654
+ }
643
655
 
644
656
  const canStart = reasons.length === 0 && !rejected;
645
657
 
@@ -662,11 +674,13 @@ export class SolveQueue {
662
674
  codexProcesses: codexProcessCount,
663
675
  agentProcesses: agentProcessCount,
664
676
  qwenProcesses: qwenProcessCount,
677
+ geminiProcesses: geminiProcessCount,
665
678
  isolatedProcesses: externalProcessing.isolatedTotal,
666
679
  totalProcessing,
667
680
  claudeProcessingCount,
668
681
  codexProcessingCount,
669
682
  qwenProcessingCount,
683
+ geminiProcessingCount,
670
684
  };
671
685
  }
672
686
 
@@ -800,10 +814,10 @@ export class SolveQueue {
800
814
  * - GitHub threshold blocks unconditionally when exceeded (ultimate restriction)
801
815
  *
802
816
  * Logic per issue #1159:
803
- * - When tool is 'agent', skip Claude-specific limits entirely since agent uses different
804
- * rate limits (Grok Code or similar). Only system resources and GitHub limits apply.
805
- * - For Claude limits, only count Claude-specific processing items, not agent items.
806
- * This allows agent tasks to run in parallel even when Claude limits are reached.
817
+ * - When tool is 'agent', 'gemini', or 'qwen', skip Claude-specific limits entirely since these tools use
818
+ * different rate limiting backends. Only system resources and GitHub limits apply.
819
+ * - For Claude limits, only count Claude-specific processing items, not agent/codex/gemini/qwen items.
820
+ * This allows non-Claude tasks to run in parallel even when Claude limits are reached.
807
821
  *
808
822
  * Logic per issue #1253:
809
823
  * - All thresholds now support configurable strategies (reject, enqueue, dequeue-one-at-a-time)
@@ -811,7 +825,7 @@ export class SolveQueue {
811
825
  *
812
826
  * @param {boolean} hasRunningToolProcess - Whether matching tool processes are running (from pgrep)
813
827
  * @param {number} toolProcessingCount - Count of matching tool items being processed in queue
814
- * @param {string} tool - The tool being used ('claude', 'agent', etc.)
828
+ * @param {string} tool - The tool being used ('claude', 'agent', 'codex', 'gemini', 'qwen', etc.)
815
829
  * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean, rejected: boolean, rejectReason: string|null}>}
816
830
  */
817
831
  async checkApiLimits(hasRunningToolProcess = false, toolProcessingCount = 0, tool = 'claude') {
@@ -821,7 +835,7 @@ export class SolveQueue {
821
835
  let rejectReason = null;
822
836
 
823
837
  // Apply Claude-specific limits only when tool is 'claude'
824
- // Other tools (like 'agent') use different rate limiting backends and are not
838
+ // Other tools (like 'agent', 'gemini', and 'qwen') use different rate limiting backends and are not
825
839
  // affected by Claude API limits (5-hour session, weekly limits)
826
840
  // See: https://github.com/link-assistant/hive-mind/issues/1159
827
841
  const applyClaudeLimits = tool === 'claude';
@@ -1427,6 +1441,9 @@ export default {
1427
1441
  getRunningProcesses,
1428
1442
  getRunningClaudeProcesses,
1429
1443
  getRunningAgentProcesses,
1444
+ getRunningCodexProcesses,
1445
+ getRunningQwenProcesses,
1446
+ getRunningGeminiProcesses,
1430
1447
  getRunningIsolatedSessions,
1431
1448
  createQueueExecuteCallback,
1432
1449
  formatDuration,
@@ -11,6 +11,9 @@ export const validateToolConnection = async ({ tool = 'claude', model, verbose =
11
11
  if (tool === 'qwen') {
12
12
  return (await import('./qwen.lib.mjs')).validateQwenConnection(model);
13
13
  }
14
+ if (tool === 'gemini') {
15
+ return (await import('./gemini.lib.mjs')).validateGeminiConnection(model);
16
+ }
14
17
  const validateClaude = validateClaudeConnection || (await import('./claude.lib.mjs')).validateClaudeConnection;
15
18
  return validateClaude(model);
16
19
  };