@link-assistant/hive-mind 1.59.7 → 1.61.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/CHANGELOG.md +12 -0
- package/package.json +3 -1
- package/src/cli-arguments.lib.mjs +68 -0
- package/src/configure-claude.lib.mjs +37 -14
- package/src/hive-screens.lib.mjs +36 -22
- package/src/hive.mjs +10 -16
- package/src/memory-check.mjs +10 -11
- package/src/review.mjs +72 -59
- package/src/reviewers-hive.mjs +108 -92
- package/src/solve.config.lib.mjs +10 -15
- package/src/start-screen.mjs +74 -15
- package/src/task.agent-command.lib.mjs +61 -0
- package/src/task.config.lib.mjs +122 -0
- package/src/task.issue-creation.lib.mjs +203 -0
- package/src/task.mjs +217 -232
- package/src/task.split.lib.mjs +221 -0
- package/src/telegram-bot.mjs +30 -111
- package/src/telegram-command-execution.lib.mjs +98 -0
- package/src/telegram-solve-queue.lib.mjs +2 -1
- package/src/telegram-task-command.lib.mjs +191 -0
|
@@ -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
|
+
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
386
|
-
return
|
|
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,17 @@ 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* - Create a GitHub issue from a repository link and issue text\n';
|
|
543
|
+
message += 'Usage: `/task <github-repository-url>` followed by issue text, or reply with `/task`\n';
|
|
544
|
+
message += 'Example: `/task https://github.com/owner/repo` then the issue text on following lines\n';
|
|
545
|
+
message += '*/split* - Split a GitHub issue into smaller issues\n';
|
|
546
|
+
message += 'Usage: `/split <github-issue-url> [options]` or `/task --split <github-issue-url>`\n';
|
|
547
|
+
message += 'Example: `/split https://github.com/owner/repo/issues/123 --split-count 2`\n\n';
|
|
548
|
+
} else {
|
|
549
|
+
message += '*/task* / */split* - ❌ Disabled\n\n';
|
|
550
|
+
}
|
|
551
|
+
|
|
636
552
|
if (hiveEnabled) {
|
|
637
553
|
message += '*/hive* - Run hive command\n';
|
|
638
554
|
message += 'Usage: `/hive <github-url> [options]`\n';
|
|
@@ -660,7 +576,7 @@ bot.command('help', async ctx => {
|
|
|
660
576
|
message += '🔔 *Session Notifications:* Completion notifications are automatic; use /subscribe for private DM forwards.\n';
|
|
661
577
|
if (ISOLATION_BACKEND) message += `🔒 *Isolation Mode:* \`${ISOLATION_BACKEND}\` (experimental)\n`;
|
|
662
578
|
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';
|
|
579
|
+
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
580
|
message += '🔧 *Common Options:*\n';
|
|
665
581
|
message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
|
|
666
582
|
message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
|
|
@@ -765,6 +681,8 @@ const { registerSolveQueueCommand } = await import('./telegram-solve-queue-comma
|
|
|
765
681
|
const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, { ...sharedCommandOpts, getSolveQueue });
|
|
766
682
|
const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.mjs'); // #1688
|
|
767
683
|
registerSubscribeCommands(bot, sharedCommandOpts);
|
|
684
|
+
const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
|
|
685
|
+
const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage });
|
|
768
686
|
|
|
769
687
|
// Named handler for /solve command - extracted for reuse by text-based fallback (issue #1207)
|
|
770
688
|
async function handleSolveCommand(ctx) {
|
|
@@ -1255,7 +1173,8 @@ bot.on('message', async (ctx, next) => {
|
|
|
1255
1173
|
|
|
1256
1174
|
// /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback — Telegraf's bot.command() is sufficient.
|
|
1257
1175
|
const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
|
|
1258
|
-
const
|
|
1176
|
+
const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
|
|
1177
|
+
const handlers = { ...solveHandlers, ...taskHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand };
|
|
1259
1178
|
|
|
1260
1179
|
const handler = handlers[extracted.command];
|
|
1261
1180
|
if (!handler) return next();
|
|
@@ -1366,7 +1285,7 @@ if (allowedChats && allowedChats.length > 0) {
|
|
|
1366
1285
|
if (allowedTopics && allowedTopics.length > 0) {
|
|
1367
1286
|
console.log('Allowed topics (lino):', lino.formatLinks(allowedTopics));
|
|
1368
1287
|
}
|
|
1369
|
-
console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled });
|
|
1288
|
+
console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
|
|
1370
1289
|
if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
|
|
1371
1290
|
if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
|
|
1372
1291
|
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
|
}
|