@link-assistant/hive-mind 1.59.6 → 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,133 @@
1
+ import { buildUserMention } from './buildUserMention.lib.mjs';
2
+ import { validateModelName } from './models/index.mjs';
3
+ import { parseTaskIssueUrl } from './task.split.lib.mjs';
4
+ import { escapeMarkdown } from './telegram-markdown.lib.mjs';
5
+ import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
6
+ import { moveArgumentToFront, parseCommandArgs } from './telegram-solve-command.lib.mjs';
7
+ import { formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
8
+
9
+ export const TASK_COMMAND_NAMES = Object.freeze(['task', 'split']);
10
+
11
+ export function getTaskCommandNameFromText(text) {
12
+ if (!text || typeof text !== 'string') return null;
13
+ const firstLine = text.split('\n')[0].trim();
14
+ const match = firstLine.match(/^\/(\w+)(?:@\S+)?(?:\s|$)/);
15
+ const command = match ? match[1].toLowerCase() : null;
16
+ return TASK_COMMAND_NAMES.includes(command) ? command : null;
17
+ }
18
+
19
+ export function applyTaskCommandDefaults(args) {
20
+ const hasSplit = args.includes('--split') || args.some(arg => arg.startsWith('--split='));
21
+ return hasSplit ? args : [...args, '--split'];
22
+ }
23
+
24
+ export function findTaskIssueUrl(args) {
25
+ return args.find(arg => !arg.startsWith('-') && parseTaskIssueUrl(arg).valid) || null;
26
+ }
27
+
28
+ export function getTaskToolFromArgs(args) {
29
+ for (let i = 0; i < args.length; i++) {
30
+ if (args[i] === '--tool' && i + 1 < args.length) return args[i + 1];
31
+ if (args[i].startsWith('--tool=')) return args[i].substring('--tool='.length);
32
+ }
33
+ return 'claude';
34
+ }
35
+
36
+ function getModelFromArgs(args) {
37
+ for (let i = 0; i < args.length; i++) {
38
+ if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
39
+ if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function validateTaskModel(args) {
45
+ const model = getModelFromArgs(args);
46
+ if (!model) return null;
47
+ const validation = validateModelName(model, getTaskToolFromArgs(args));
48
+ return validation.valid ? null : validation.message;
49
+ }
50
+
51
+ export function buildTaskCommandArgs(text) {
52
+ const args = applyTaskCommandDefaults(parseCommandArgs(text));
53
+ const issueUrl = findTaskIssueUrl(args);
54
+ return {
55
+ args: issueUrl ? moveArgumentToFront(args, issueUrl) : args,
56
+ issueUrl,
57
+ };
58
+ }
59
+
60
+ export function registerTaskCommands(bot, options) {
61
+ const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage } = options;
62
+
63
+ async function handleTaskCommand(ctx) {
64
+ const commandName = getTaskCommandNameFromText(ctx.message?.text) || 'task';
65
+ const commandDisplay = `/${commandName}`;
66
+ VERBOSE && console.log(`[VERBOSE] ${commandDisplay} command received`);
67
+
68
+ await addBreadcrumb({
69
+ category: 'telegram.command',
70
+ message: `${commandDisplay} command received`,
71
+ level: 'info',
72
+ data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
73
+ });
74
+
75
+ if (!taskEnabled) {
76
+ await ctx.reply('❌ The task command is disabled on this bot instance.');
77
+ return;
78
+ }
79
+ if (isOldMessage(ctx)) return;
80
+ if (!isGroupChat(ctx)) {
81
+ await ctx.reply(`❌ The ${commandDisplay} command only works in group chats. Please add this bot to a group and make it an admin.`, { reply_to_message_id: ctx.message.message_id });
82
+ return;
83
+ }
84
+ if (!isTopicAuthorized(ctx)) {
85
+ await ctx.reply(buildAuthErrorMessage(ctx), { reply_to_message_id: ctx.message.message_id });
86
+ return;
87
+ }
88
+ if (isChatStopped(ctx.chat.id)) {
89
+ await safeReply(ctx, getStoppedChatRejectMessage(ctx.chat.id, 'Task'), { reply_to_message_id: ctx.message.message_id });
90
+ return;
91
+ }
92
+
93
+ const built = buildTaskCommandArgs(ctx.message.text);
94
+ if (!built.issueUrl) {
95
+ await safeReply(ctx, `❌ Missing GitHub issue URL. Usage: \`${commandDisplay} <github-issue-url> [options]\`\n\nExample: \`${commandDisplay} https://github.com/owner/repo/issues/123\``, { reply_to_message_id: ctx.message.message_id });
96
+ return;
97
+ }
98
+
99
+ const parsedIssue = parseTaskIssueUrl(built.issueUrl);
100
+ if (!parsedIssue.valid) {
101
+ await safeReply(ctx, `❌ ${escapeMarkdown(parsedIssue.error || 'Invalid GitHub issue URL')}`, { reply_to_message_id: ctx.message.message_id });
102
+ return;
103
+ }
104
+
105
+ const { backend: perCommandIsolation, filteredArgs } = extractIsolationFromArgs(built.args);
106
+ if (perCommandIsolation && !isValidPerCommandIsolation(perCommandIsolation)) {
107
+ await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(perCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
108
+ return;
109
+ }
110
+
111
+ const modelError = validateTaskModel(filteredArgs);
112
+ if (modelError) {
113
+ await safeReply(ctx, `❌ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
114
+ return;
115
+ }
116
+
117
+ const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
118
+ const userOptionsRaw = built.args.slice(1).join(' ');
119
+ let infoBlock = `Requested by: ${requester}\nIssue: ${escapeMarkdown(built.issueUrl)}`;
120
+ if (userOptionsRaw) infoBlock += `\n\n🛠 Options: ${escapeMarkdown(userOptionsRaw)}`;
121
+
122
+ const taskUrlContext = { owner: parsedIssue.owner, repo: parsedIssue.repo, number: parsedIssue.number, type: parsedIssue.type, normalized: parsedIssue.normalized || built.issueUrl };
123
+ const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
124
+ await executeAndUpdateMessage(ctx, startingMessage, 'task', filteredArgs, infoBlock, perCommandIsolation || null, getTaskToolFromArgs(filteredArgs), taskUrlContext);
125
+ }
126
+
127
+ bot.command(
128
+ TASK_COMMAND_NAMES.map(command => new RegExp(`^${command}$`, 'i')),
129
+ handleTaskCommand
130
+ );
131
+
132
+ return { handleTaskCommand, TASK_COMMAND_NAMES };
133
+ }
@@ -55,6 +55,17 @@ export const AUTO_MERGED_MARKER = 'Auto-merged';
55
55
  // solve.auto-merge.lib.mjs — billing-limit notification (spending cap / free tier)
56
56
  export const BILLING_LIMIT_MARKER = 'GitHub Actions Billing Limit';
57
57
 
58
+ // solve.results.lib.mjs — working session summary comments posted by
59
+ // --attach-solution-summary / --auto-attach-solution-summary at the end of
60
+ // every working session (top-level solve, auto-restart-until-mergeable
61
+ // iteration, or watch-mode iteration). Issue #1728: Renamed from
62
+ // "Solution summary" because not every working session is a solution draft —
63
+ // many are continuation/restart iterations that are part of an in-progress
64
+ // solution. Tracking it as a tool-generated marker prevents the next
65
+ // iteration's --auto-attach-solution-summary check from mistaking a
66
+ // previous iteration's summary for an AI-authored comment.
67
+ export const WORKING_SESSION_SUMMARY_MARKER = 'Working session summary';
68
+
58
69
  // github.lib.mjs — fork contributor "Allow edits by maintainers" request
59
70
  export const MAINTAINER_ACCESS_REQUEST_MARKER = 'Allow edits by maintainers';
60
71
 
@@ -88,7 +99,7 @@ export const USAGE_LIMIT_REACHED_MARKER = 'Usage Limit Reached';
88
99
  * named constants above so that adding a new marker only requires adding
89
100
  * the constant and appending it here.
90
101
  */
91
- export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER];
102
+ export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, WORKING_SESSION_SUMMARY_MARKER];
92
103
 
93
104
  /**
94
105
  * Markers that indicate the end of a working session. Used by