@link-assistant/hive-mind 2.10.4 → 2.11.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.
@@ -2,6 +2,8 @@ import { buildUserMention } from './buildUserMention.lib.mjs';
2
2
  import { validateModelName } from './models/index.mjs';
3
3
  import { getLinoYargsFactory } from './cli-arguments.lib.mjs';
4
4
  import { createYargsConfig as createTaskYargsConfig } from './task.config.lib.mjs';
5
+ import { createCiCdIssue } from './fix.ci-cd-issue.lib.mjs';
6
+ import { parseFixRepository } from './fix.ci-cd.lib.mjs';
5
7
  import { createTaskIssue, parseTaskIssueCreationInput, resolveTaskIssueCreationInput } from './task.issue-creation.lib.mjs';
6
8
  import { parseTaskIssueUrl } from './task.split.lib.mjs';
7
9
  import { escapeMarkdown } from './telegram-markdown.lib.mjs';
@@ -23,6 +25,10 @@ export function hasTaskSplitFlag(args) {
23
25
  return args.includes('--split') || args.some(arg => arg.startsWith('--split='));
24
26
  }
25
27
 
28
+ export function hasTaskCiCdFlag(args) {
29
+ return args.includes('--ci-cd');
30
+ }
31
+
26
32
  export function applyTaskCommandDefaults(args, commandName = 'task') {
27
33
  if (commandName !== 'split') return args;
28
34
  const hasSplit = args.includes('--split') || args.some(arg => arg.startsWith('--split='));
@@ -66,6 +72,16 @@ export function buildTaskCommandArgs(text) {
66
72
  };
67
73
  }
68
74
 
75
+ export function buildTaskCiCdCommandArgs(text) {
76
+ const args = parseCommandArgs(text);
77
+ const repositoryRaw = args.find(arg => !arg.startsWith('-') && parseFixRepository(arg)) || null;
78
+ return {
79
+ args,
80
+ repositoryRaw,
81
+ repository: repositoryRaw ? parseFixRepository(repositoryRaw) : null,
82
+ };
83
+ }
84
+
69
85
  function getReplyText(message) {
70
86
  const reply = message?.reply_to_message;
71
87
  if (!reply || reply.forum_topic_created) return '';
@@ -97,7 +113,7 @@ function injectLanguageIfMissing(args, locale) {
97
113
  }
98
114
 
99
115
  export function registerTaskCommands(bot, options) {
100
- const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isForwarded, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, createTaskIssue: createTaskIssueFn = createTaskIssue, resolveLocale = null } = options;
116
+ const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isForwarded, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, createTaskIssue: createTaskIssueFn = createTaskIssue, createCiCdIssue: createCiCdIssueFn = createCiCdIssue, resolveLocale = null } = options;
101
117
 
102
118
  async function handleTaskCommand(ctx) {
103
119
  const commandName = getTaskCommandNameFromText(ctx.message?.text) || 'task';
@@ -138,6 +154,36 @@ export function registerTaskCommands(bot, options) {
138
154
 
139
155
  const parsedArgs = parseCommandArgs(ctx.message.text);
140
156
  const splitMode = commandName === 'split' || hasTaskSplitFlag(parsedArgs);
157
+ const ciCdMode = commandName === 'task' && hasTaskCiCdFlag(parsedArgs);
158
+
159
+ if (splitMode && ciCdMode) {
160
+ await safeReply(ctx, '❌ `--ci-cd` and `--split` cannot be used together.', { reply_to_message_id: ctx.message.message_id });
161
+ return;
162
+ }
163
+
164
+ if (ciCdMode) {
165
+ const built = buildTaskCiCdCommandArgs(ctx.message.text);
166
+ if (!built.repository) {
167
+ await safeReply(ctx, `❌ Missing GitHub repository URL. Usage: \`${commandDisplay} --ci-cd <github-repository-url>\`\n\nExample: \`${commandDisplay} --ci-cd https://github.com/owner/repo\``, { reply_to_message_id: ctx.message.message_id });
168
+ return;
169
+ }
170
+
171
+ const statusMessage = await ctx.reply(`Collecting CI/CD context and creating a GitHub issue in ${built.repository.fullName}...`, {
172
+ reply_to_message_id: ctx.message.message_id,
173
+ disable_web_page_preview: true,
174
+ });
175
+
176
+ try {
177
+ const createdIssue = await createCiCdIssueFn({
178
+ repository: built.repository,
179
+ log: message => VERBOSE && console.log(`[VERBOSE] ${message}`),
180
+ });
181
+ await editTelegramMessage(ctx, statusMessage, `Created GitHub issue:\n${createdIssue.url}\n\nReply to this message with /solve --development-log --deep-analysis --auto-merge to continue the full CI/CD remediation workflow.`);
182
+ } catch (error) {
183
+ await editTelegramMessage(ctx, statusMessage, `Error creating CI/CD issue:\n${error.message || String(error)}`);
184
+ }
185
+ return;
186
+ }
141
187
 
142
188
  if (!splitMode) {
143
189
  const replyText = getReplyText(ctx.message);
@@ -128,7 +128,7 @@ export function appendPullRequestLine(infoBlock, pullRequestUrl, { locale = null
128
128
  return [...before, prLine, ...after].join('\n');
129
129
  }
130
130
 
131
- export function formatSessionCompletionMessage({ sessionName, sessionInfo, statusResult = null, observedEndTime = new Date(), exitCode = null, infoBlock = '', pullRequestUrl = null, extraSections = [], locale = null } = {}) {
131
+ export function formatSessionCompletionMessage({ sessionName, sessionInfo, statusResult = null, observedEndTime = new Date(), exitCode = null, infoBlock = '', pullRequestUrl = null, pullRequestState = null, extraSections = [], locale = null } = {}) {
132
132
  const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
133
133
  const outcome = classifySessionOutcome({ exitCode: finalExitCode, status: statusResult?.status || null });
134
134
  const { failed, killed, signal } = outcome;
@@ -142,6 +142,7 @@ export function formatSessionCompletionMessage({ sessionName, sessionInfo, statu
142
142
  // is an orderly, intentional termination, so surface it as such regardless of
143
143
  // which signal actually delivered the kill.
144
144
  const stopRequestedByUser = Boolean(sessionInfo?.stopRequestedByUser);
145
+ const pullRequestMerged = pullRequestState?.merged === true || Boolean(pullRequestState?.mergedAt);
145
146
  let statusEmojiOverride = null;
146
147
  let statusText;
147
148
  if (killed && stopRequestedByUser) {
@@ -158,6 +159,14 @@ export function formatSessionCompletionMessage({ sessionName, sessionInfo, statu
158
159
  const exitSuffix = showCode ? ` (exit code: ${finalExitCode})` : '';
159
160
  const reason = signal ? signal.reason : 'killed';
160
161
  statusText = text(messageLocale, 'telegram.work_session_killed', `Work session ${reason}${exitSuffix}`, { reason, exitCode: finalExitCode ?? '', signal: signal?.signal ?? '', exitSuffix });
162
+ } else if (failed && pullRequestMerged) {
163
+ // Issue #2117: the runner's exit code is still authoritative and must not
164
+ // be hidden, but calling the entire session "failed" contradicts the
165
+ // externally verified result when its pull request has already merged.
166
+ // Describe both outcomes so operators know the requested goal completed
167
+ // and that a later orchestration failure still needs investigation.
168
+ statusEmojiOverride = '⚠️';
169
+ statusText = text(messageLocale, 'telegram.work_session_merged_but_failed', `Pull request merged, but the work session exited with code: ${finalExitCode}`, { exitCode: finalExitCode });
161
170
  } else if (failed) {
162
171
  statusText = text(messageLocale, 'telegram.work_session_failed', `Work session failed (exit code: ${finalExitCode})`, { exitCode: finalExitCode });
163
172
  } else {
@@ -183,7 +192,8 @@ export function formatSessionCompletionMessage({ sessionName, sessionInfo, statu
183
192
 
184
193
  // Issue #594: --show-limits virtual option appends snapshot/delta sections
185
194
  // (Markdown code blocks) below the standard completion details.
186
- const extras = (Array.isArray(extraSections) ? extraSections : []).filter(Boolean);
195
+ const mergedWithRunnerFailureSection = failed && !killed && pullRequestMerged ? [`✅ ${text(messageLocale, 'telegram.work_session_merged_success', 'The requested pull request was merged successfully.')}`, `⚠️ ${text(messageLocale, 'telegram.work_session_runner_also_failed', 'The runner also failed; its exit code is preserved for investigation.')}`].join('\n') : null;
196
+ const extras = [mergedWithRunnerFailureSection, ...(Array.isArray(extraSections) ? extraSections : [])].filter(Boolean);
187
197
  if (extras.length > 0) {
188
198
  message += `\n\n${extras.join('\n\n')}`;
189
199
  }