@link-assistant/hive-mind 1.59.7 → 1.60.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.
@@ -0,0 +1,221 @@
1
+ import { parseGitHubUrl } from './github.lib.mjs';
2
+
3
+ export const TASK_SPLIT_MARKER_START = '<!-- hive-mind-task-split:start -->';
4
+ export const TASK_SPLIT_MARKER_END = '<!-- hive-mind-task-split:end -->';
5
+ export const GITHUB_SUB_ISSUES_API_VERSION = '2026-03-10';
6
+
7
+ export function parseTaskIssueUrl(url) {
8
+ const parsed = parseGitHubUrl(url);
9
+ if (!parsed.valid) {
10
+ return parsed;
11
+ }
12
+ if (parsed.type !== 'issue') {
13
+ return {
14
+ valid: false,
15
+ error: parsed.type === 'pull' ? 'The task command accepts GitHub issues, not pull requests' : 'The task command requires a specific GitHub issue URL',
16
+ parsed,
17
+ };
18
+ }
19
+ return parsed;
20
+ }
21
+
22
+ export function buildTaskSplitSystemPrompt() {
23
+ return ['You split GitHub issues into smaller GitHub issues.', 'Read only the issue details supplied by the user.', 'Do not clone repositories, create branches, edit files, create GitHub issues, or execute shell commands.', 'Return only valid JSON matching the requested schema.'].join('\n');
24
+ }
25
+
26
+ export function buildTaskSplitPrompt({ issue, splitCount }) {
27
+ return `Split this GitHub issue into exactly ${splitCount} smaller GitHub issues.
28
+
29
+ Source issue:
30
+ - Repository: ${issue.owner}/${issue.repo}
31
+ - Issue number: ${issue.number}
32
+ - URL: ${issue.url}
33
+ - Title: ${issue.title}
34
+
35
+ Issue body:
36
+ ${issue.body || '(empty)'}
37
+
38
+ Return only this JSON shape:
39
+ {
40
+ "tasks": [
41
+ {
42
+ "title": "short issue title",
43
+ "body": "complete issue body with objective, scope, deliverables, and acceptance criteria",
44
+ "dependencies": [1]
45
+ }
46
+ ]
47
+ }
48
+
49
+ Rules:
50
+ - The tasks array must contain exactly ${splitCount} items.
51
+ - Each task must be independently actionable.
52
+ - Together the tasks must cover the full source issue.
53
+ - Dependencies must be 1-based task numbers and should be empty when none are needed.
54
+ - Do not include Markdown fences, prose, comments, or extra top-level keys.`;
55
+ }
56
+
57
+ function tryParseJson(text) {
58
+ try {
59
+ return JSON.parse(text);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function extractFencedBlocks(text) {
66
+ const blocks = [];
67
+ const regex = /```(?:json)?\s*([\s\S]*?)```/gi;
68
+ let match;
69
+ while ((match = regex.exec(text)) !== null) {
70
+ blocks.push(match[1].trim());
71
+ }
72
+ return blocks;
73
+ }
74
+
75
+ function findBalancedJsonCandidates(text) {
76
+ const candidates = [];
77
+ const stack = [];
78
+ let start = -1;
79
+ let inString = false;
80
+ let escape = false;
81
+
82
+ for (let i = 0; i < text.length; i++) {
83
+ const char = text[i];
84
+ if (inString) {
85
+ if (escape) escape = false;
86
+ else if (char === '\\') escape = true;
87
+ else if (char === '"') inString = false;
88
+ continue;
89
+ }
90
+ if (char === '"') {
91
+ inString = true;
92
+ continue;
93
+ }
94
+ if (char === '{' || char === '[') {
95
+ if (stack.length === 0) start = i;
96
+ stack.push(char);
97
+ } else if ((char === '}' || char === ']') && stack.length > 0) {
98
+ const open = stack.pop();
99
+ if ((open === '{' && char !== '}') || (open === '[' && char !== ']')) {
100
+ stack.length = 0;
101
+ start = -1;
102
+ continue;
103
+ }
104
+ if (stack.length === 0 && start >= 0) {
105
+ candidates.push(text.slice(start, i + 1));
106
+ start = -1;
107
+ }
108
+ }
109
+ }
110
+
111
+ return candidates;
112
+ }
113
+
114
+ function collectStringValues(value, out = []) {
115
+ if (typeof value === 'string') {
116
+ out.push(value);
117
+ } else if (Array.isArray(value)) {
118
+ for (const item of value) collectStringValues(item, out);
119
+ } else if (value && typeof value === 'object') {
120
+ for (const item of Object.values(value)) collectStringValues(item, out);
121
+ }
122
+ return out;
123
+ }
124
+
125
+ export function extractTaskSplitJson(output) {
126
+ const candidates = [output.trim(), ...extractFencedBlocks(output), ...findBalancedJsonCandidates(output)];
127
+
128
+ for (const candidate of candidates) {
129
+ const parsed = tryParseJson(candidate);
130
+ if (parsed && (Array.isArray(parsed) || Array.isArray(parsed.tasks))) return parsed;
131
+ }
132
+
133
+ for (const line of output.split('\n')) {
134
+ const parsedLine = tryParseJson(line.trim());
135
+ if (!parsedLine) continue;
136
+ for (const value of collectStringValues(parsedLine)) {
137
+ for (const candidate of [value, ...extractFencedBlocks(value), ...findBalancedJsonCandidates(value)]) {
138
+ const parsed = tryParseJson(candidate.trim());
139
+ if (parsed && (Array.isArray(parsed) || Array.isArray(parsed.tasks))) return parsed;
140
+ }
141
+ }
142
+ }
143
+
144
+ throw new Error('AI output did not contain valid task split JSON');
145
+ }
146
+
147
+ export function normalizeSplitTasks(parsed, splitCount) {
148
+ const tasks = Array.isArray(parsed) ? parsed : parsed?.tasks;
149
+ if (!Array.isArray(tasks)) {
150
+ throw new Error('Split JSON must contain a tasks array');
151
+ }
152
+ if (tasks.length !== splitCount) {
153
+ throw new Error(`Expected exactly ${splitCount} split tasks, received ${tasks.length}`);
154
+ }
155
+
156
+ return tasks.map((task, index) => {
157
+ const title = String(task?.title || '').trim();
158
+ const body = String(task?.body || task?.description || '').trim();
159
+ if (!title) throw new Error(`Split task ${index + 1} is missing a title`);
160
+ if (!body) throw new Error(`Split task ${index + 1} is missing a body`);
161
+
162
+ const dependencies = Array.isArray(task.dependencies) ? task.dependencies.map(value => Number(value)).filter(value => Number.isInteger(value) && value >= 1 && value <= splitCount && value !== index + 1) : [];
163
+
164
+ return {
165
+ title: title.slice(0, 256),
166
+ body,
167
+ dependencies: [...new Set(dependencies)],
168
+ };
169
+ });
170
+ }
171
+
172
+ export function formatChildIssueBody({ parentIssue, task, index, splitCount }) {
173
+ const dependencyText = task.dependencies.length > 0 ? task.dependencies.map(number => `Task ${number}`).join(', ') : 'None';
174
+ return `${task.body}
175
+
176
+ ---
177
+
178
+ Split from: ${parentIssue.url}
179
+ Parent issue: #${parentIssue.number}
180
+ Split task: ${index + 1} of ${splitCount}
181
+ Dependencies: ${dependencyText}`;
182
+ }
183
+
184
+ export function formatParentSplitSection({ childIssues }) {
185
+ const lines = [TASK_SPLIT_MARKER_START, '## Split Tasks', '', ...childIssues.map((issue, index) => `- [ ] #${issue.number} ${issue.title || `Split task ${index + 1}`}`), TASK_SPLIT_MARKER_END];
186
+ return lines.join('\n');
187
+ }
188
+
189
+ export function appendOrReplaceParentSplitSection(body, childIssues) {
190
+ const section = formatParentSplitSection({ childIssues });
191
+ const currentBody = body || '';
192
+ const start = currentBody.indexOf(TASK_SPLIT_MARKER_START);
193
+ const end = currentBody.indexOf(TASK_SPLIT_MARKER_END);
194
+
195
+ if (start >= 0 && end > start) {
196
+ return `${currentBody.slice(0, start).trimEnd()}\n\n${section}\n\n${currentBody.slice(end + TASK_SPLIT_MARKER_END.length).trimStart()}`.trim();
197
+ }
198
+
199
+ return `${currentBody.trimEnd()}\n\n${section}`.trim();
200
+ }
201
+
202
+ export function parseCreatedIssueUrl(url) {
203
+ const parsed = parseGitHubUrl(url);
204
+ if (!parsed.valid || parsed.type !== 'issue') {
205
+ throw new Error(`Could not parse created issue URL: ${url}`);
206
+ }
207
+ return parsed;
208
+ }
209
+
210
+ export function buildIssueRestIdApiArgs(issue) {
211
+ return ['api', `repos/${issue.owner}/${issue.repo}/issues/${issue.number}`, '--jq', '.id'];
212
+ }
213
+
214
+ export function buildAddSubIssueApiArgs({ parentIssue, subIssueId }) {
215
+ const numericId = Number(subIssueId);
216
+ if (!Number.isInteger(numericId) || numericId <= 0) {
217
+ throw new Error(`Invalid sub-issue REST id: ${subIssueId}`);
218
+ }
219
+
220
+ return ['api', '-X', 'POST', `repos/${parentIssue.owner}/${parentIssue.repo}/issues/${parentIssue.number}/sub_issues`, '-H', 'Accept: application/vnd.github+json', '-H', `X-GitHub-Api-Version: ${GITHUB_SUB_ISSUES_API_VERSION}`, '-F', `sub_issue_id=${numericId}`];
221
+ }
@@ -6,12 +6,6 @@ if (process.argv.includes('--version')) {
6
6
  process.exit(v === 'unknown' ? 1 : 0);
7
7
  }
8
8
 
9
- import { spawn } from 'child_process';
10
- import { promisify } from 'util';
11
- import { exec as execCallback } from 'child_process';
12
-
13
- const exec = promisify(execCallback);
14
-
15
9
  if (typeof use === 'undefined') {
16
10
  globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
17
11
  }
@@ -20,22 +14,16 @@ const { lino } = await import('./lino.lib.mjs');
20
14
  const { buildUserMention } = await import('./buildUserMention.lib.mjs');
21
15
  const { reportError, initializeSentry, addBreadcrumb } = await import('./sentry.lib.mjs');
22
16
  const { loadLenvConfig } = await import('./lenv-reader.lib.mjs');
17
+ const { getLinoYargsFactory, getenv, hideBin } = await import('./cli-arguments.lib.mjs');
23
18
 
24
19
  const dotenvxModule = await use('@dotenvx/dotenvx');
25
20
  const dotenvx = dotenvxModule.default || dotenvxModule;
26
- const getenvModule = await use('getenv');
27
- const getenv = typeof getenvModule === 'function' ? getenvModule : getenvModule.default || getenvModule;
28
- const { resolveYargsFactory } = await import('./yargs-factory.lib.mjs');
29
21
 
30
22
  // Load .env/.lenv configuration (issue #1318)
31
23
  dotenvx.config({ quiet: true, ignore: ['MISSING_ENV_FILE'] });
32
24
  await loadLenvConfig({ override: true, quiet: true });
33
25
 
34
- const yargsModule = await use('yargs@17.7.2');
35
- const yargs = resolveYargsFactory(yargsModule);
36
- const helpersModuleBot = await use('yargs@17.7.2/helpers');
37
- const _helpersBot = helpersModuleBot.default || helpersModuleBot;
38
- const hideBin = _helpersBot.hideBin || (argv => argv.slice(2));
26
+ const yargs = getLinoYargsFactory();
39
27
  const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
40
28
  const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
41
29
  const { parseGitHubUrl, validateGitHubEntityExistence } = await import('./github.lib.mjs');
@@ -47,6 +35,7 @@ const { getVersionInfo, formatVersionMessage } = await import('./version-info.li
47
35
  const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
48
36
  const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
49
37
  const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFromText, getSolveToolAliasFromText, moveArgumentToFront, parseArgsWithYargs, parseCommandArgs, SOLVE_COMMAND_NAMES } = await import('./telegram-solve-command.lib.mjs');
38
+ const { executeStartScreen: executeStartScreenCommand } = await import('./telegram-command-execution.lib.mjs');
50
39
  const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
51
40
  const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
52
41
  const { safeReply } = await import('./telegram-safe-reply.lib.mjs');
@@ -103,6 +92,11 @@ const config = yargs(hideBin(process.argv))
103
92
  description: 'Enable /hive command (use --no-hive to disable)',
104
93
  default: getenv('TELEGRAM_HIVE', 'true') !== 'false',
105
94
  })
95
+ .option('task', {
96
+ type: 'boolean',
97
+ description: 'Enable /task and /split commands (use --no-task to disable)',
98
+ default: getenv('TELEGRAM_TASK', 'true') !== 'false',
99
+ })
106
100
  .option('dryRun', {
107
101
  type: 'boolean',
108
102
  description: 'Validate configuration and options without starting the bot',
@@ -162,6 +156,7 @@ const hiveOverrides = resolvedHiveOverrides
162
156
  : [];
163
157
  const solveEnabled = config.solve;
164
158
  const hiveEnabled = config.hive;
159
+ const taskEnabled = config.task;
165
160
  // Isolation mode (experimental): uses `$` from start-command with specified backend
166
161
  const ISOLATION_BACKEND = (config.isolation || getenv('TELEGRAM_ISOLATION', '')).trim().toLowerCase();
167
162
  let isolationRunner = null;
@@ -282,7 +277,7 @@ if (config.dryRun) {
282
277
  if (allowedTopics && allowedTopics.length > 0) {
283
278
  console.log(' Allowed topics:', lino.formatLinks(allowedTopics));
284
279
  }
285
- console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled });
280
+ console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
286
281
  if (solveOverrides.length > 0) {
287
282
  console.log(' Solve overrides:', lino.format(solveOverrides));
288
283
  }
@@ -336,102 +331,12 @@ function isOldMessage(ctx) {
336
331
  return _isOldMessage(ctx, BOT_START_TIME, { verbose: VERBOSE });
337
332
  }
338
333
 
339
- function isForwardedOrReply(ctx) {
340
- return _isForwardedOrReply(ctx, { verbose: VERBOSE });
341
- }
342
-
343
- async function findStartScreenCommand() {
344
- try {
345
- const { stdout } = await exec('which start-screen');
346
- return stdout.trim();
347
- } catch {
348
- return null;
349
- }
350
- }
351
-
352
334
  async function executeStartScreen(command, args) {
353
- try {
354
- // Check if start-screen is available BEFORE first execution
355
- const whichPath = await findStartScreenCommand();
356
-
357
- if (!whichPath) {
358
- const warningMsg = '⚠️ WARNING: start-screen command not found in PATH\n' + 'Please ensure @link-assistant/hive-mind is properly installed\n' + 'You may need to run: npm install -g @link-assistant/hive-mind';
359
- console.warn(warningMsg);
360
-
361
- // Still try to execute with 'start-screen' in case it's available in PATH but 'which' failed
362
- return {
363
- success: false,
364
- warning: warningMsg,
365
- error: 'start-screen command not found in PATH',
366
- };
367
- }
368
-
369
- // Use the resolved path from which
370
- if (VERBOSE) {
371
- console.log(`[VERBOSE] Found start-screen at: ${whichPath}`);
372
- }
373
-
374
- return await executeWithCommand(whichPath, command, args);
375
- } catch (error) {
376
- console.error('Error executing start-screen:', error);
377
- return {
378
- success: false,
379
- output: '',
380
- error: error.message,
381
- };
382
- }
335
+ return executeStartScreenCommand(command, args, { verbose: VERBOSE });
383
336
  }
384
337
 
385
- function executeWithCommand(startScreenCmd, command, args) {
386
- return new Promise(resolve => {
387
- const allArgs = [command, ...args];
388
-
389
- if (VERBOSE) {
390
- console.log(`[VERBOSE] Executing: ${startScreenCmd} ${allArgs.join(' ')}`);
391
- } else {
392
- console.log(`Executing: ${startScreenCmd} ${allArgs.join(' ')}`);
393
- }
394
-
395
- const child = spawn(startScreenCmd, allArgs, {
396
- stdio: ['ignore', 'pipe', 'pipe'],
397
- detached: false,
398
- env: process.env,
399
- });
400
-
401
- let stdout = '';
402
- let stderr = '';
403
-
404
- child.stdout.on('data', data => {
405
- stdout += data.toString();
406
- });
407
-
408
- child.stderr.on('data', data => {
409
- stderr += data.toString();
410
- });
411
-
412
- child.on('error', error => {
413
- resolve({
414
- success: false,
415
- output: stdout,
416
- error: error.message,
417
- });
418
- });
419
-
420
- child.on('close', code => {
421
- if (code === 0) {
422
- resolve({
423
- success: true,
424
- output: stdout,
425
- });
426
- } else {
427
- resolve({
428
- success: false,
429
- output: stdout,
430
- error: stderr || `Command exited with code ${code}`,
431
- });
432
- }
433
- });
434
- });
338
+ function isForwardedOrReply(ctx) {
339
+ return _isForwardedOrReply(ctx, { verbose: VERBOSE });
435
340
  }
436
341
 
437
342
  /**
@@ -633,6 +538,14 @@ bot.command('help', async ctx => {
633
538
  message += '*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*) - ❌ Disabled\n\n';
634
539
  }
635
540
 
541
+ if (taskEnabled) {
542
+ message += '*/task* / */split* - Split a GitHub issue into smaller issues\n';
543
+ message += 'Usage: `/split <github-issue-url> [options]`\n';
544
+ message += 'Example: `/split https://github.com/owner/repo/issues/123 --split-count 2`\n\n';
545
+ } else {
546
+ message += '*/task* / */split* - ❌ Disabled\n\n';
547
+ }
548
+
636
549
  if (hiveEnabled) {
637
550
  message += '*/hive* - Run hive command\n';
638
551
  message += 'Usage: `/hive <github-url> [options]`\n';
@@ -660,7 +573,7 @@ bot.command('help', async ctx => {
660
573
  message += '🔔 *Session Notifications:* Completion notifications are automatic; use /subscribe for private DM forwards.\n';
661
574
  if (ISOLATION_BACKEND) message += `🔒 *Isolation Mode:* \`${ISOLATION_BACKEND}\` (experimental)\n`;
662
575
  message += '\n';
663
- message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /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';
576
+ message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /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';
664
577
  message += '🔧 *Common Options:*\n';
665
578
  message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
666
579
  message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
@@ -765,6 +678,8 @@ const { registerSolveQueueCommand } = await import('./telegram-solve-queue-comma
765
678
  const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, { ...sharedCommandOpts, getSolveQueue });
766
679
  const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.mjs'); // #1688
767
680
  registerSubscribeCommands(bot, sharedCommandOpts);
681
+ const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
682
+ const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage });
768
683
 
769
684
  // Named handler for /solve command - extracted for reuse by text-based fallback (issue #1207)
770
685
  async function handleSolveCommand(ctx) {
@@ -1255,7 +1170,8 @@ bot.on('message', async (ctx, next) => {
1255
1170
 
1256
1171
  // /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback — Telegraf's bot.command() is sufficient.
1257
1172
  const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
1258
- const handlers = { ...solveHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand };
1173
+ const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
1174
+ const handlers = { ...solveHandlers, ...taskHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand };
1259
1175
 
1260
1176
  const handler = handlers[extracted.command];
1261
1177
  if (!handler) return next();
@@ -1366,7 +1282,7 @@ if (allowedChats && allowedChats.length > 0) {
1366
1282
  if (allowedTopics && allowedTopics.length > 0) {
1367
1283
  console.log('Allowed topics (lino):', lino.formatLinks(allowedTopics));
1368
1284
  }
1369
- console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled });
1285
+ console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
1370
1286
  if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
1371
1287
  if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1372
1288
  if (VERBOSE) {
@@ -0,0 +1,98 @@
1
+ import { spawn } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { exec as execCallback } from 'child_process';
4
+
5
+ const exec = promisify(execCallback);
6
+
7
+ async function findStartScreenCommand() {
8
+ try {
9
+ const { stdout } = await exec('which start-screen');
10
+ return stdout.trim();
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ function executeWithCommand(startScreenCmd, command, args, verbose = false) {
17
+ return new Promise(resolve => {
18
+ const allArgs = [command, ...args];
19
+
20
+ if (verbose) {
21
+ console.log(`[VERBOSE] Executing: ${startScreenCmd} ${allArgs.join(' ')}`);
22
+ } else {
23
+ console.log(`Executing: ${startScreenCmd} ${allArgs.join(' ')}`);
24
+ }
25
+
26
+ const child = spawn(startScreenCmd, allArgs, {
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ detached: false,
29
+ env: process.env,
30
+ });
31
+
32
+ let stdout = '';
33
+ let stderr = '';
34
+
35
+ child.stdout.on('data', data => {
36
+ stdout += data.toString();
37
+ });
38
+
39
+ child.stderr.on('data', data => {
40
+ stderr += data.toString();
41
+ });
42
+
43
+ child.on('error', error => {
44
+ resolve({
45
+ success: false,
46
+ output: stdout,
47
+ error: error.message,
48
+ });
49
+ });
50
+
51
+ child.on('close', code => {
52
+ if (code === 0) {
53
+ resolve({
54
+ success: true,
55
+ output: stdout,
56
+ });
57
+ } else {
58
+ resolve({
59
+ success: false,
60
+ output: stdout,
61
+ error: stderr || `Command exited with code ${code}`,
62
+ });
63
+ }
64
+ });
65
+ });
66
+ }
67
+
68
+ export async function executeStartScreen(command, args, options = {}) {
69
+ const { verbose = false } = options;
70
+
71
+ try {
72
+ const whichPath = await findStartScreenCommand();
73
+
74
+ if (!whichPath) {
75
+ const warningMsg = '⚠️ WARNING: start-screen command not found in PATH\n' + 'Please ensure @link-assistant/hive-mind is properly installed\n' + 'You may need to run: npm install -g @link-assistant/hive-mind';
76
+ console.warn(warningMsg);
77
+
78
+ return {
79
+ success: false,
80
+ warning: warningMsg,
81
+ error: 'start-screen command not found in PATH',
82
+ };
83
+ }
84
+
85
+ if (verbose) {
86
+ console.log(`[VERBOSE] Found start-screen at: ${whichPath}`);
87
+ }
88
+
89
+ return await executeWithCommand(whichPath, command, args, verbose);
90
+ } catch (error) {
91
+ console.error('Error executing start-screen:', error);
92
+ return {
93
+ success: false,
94
+ output: '',
95
+ error: error.message,
96
+ };
97
+ }
98
+ }
@@ -141,6 +141,7 @@ export class SolveQueue {
141
141
  this.messageUpdateCallback = options.messageUpdateCallback || null;
142
142
  this.getRunningProcessesFn = options.getRunningProcesses || getRunningProcesses;
143
143
  this.getRunningIsolatedSessionsFn = options.getRunningIsolatedSessions || getRunningIsolatedSessions;
144
+ this.autoStart = options.autoStart !== false;
144
145
 
145
146
  // Separate queues per tool type - claude tasks never block agent tasks
146
147
  // See: https://github.com/link-assistant/hive-mind/issues/1159
@@ -241,7 +242,7 @@ export class SolveQueue {
241
242
  this.log(`Enqueued: ${item.toString()} to ${item.tool} queue, queue length: ${toolQueue.length}`);
242
243
 
243
244
  // Start consumer if not already running
244
- this.ensureConsumerRunning();
245
+ if (this.autoStart) this.ensureConsumerRunning();
245
246
 
246
247
  return item;
247
248
  }