@link-assistant/hive-mind 2.10.5 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3613953: Add `/task --ci-cd <repository>` as a CI/CD remediation issue-only fallback for `/fix`.
8
+
3
9
  ## 2.10.5
4
10
 
5
11
  ### Patch Changes
package/README.hi.md CHANGED
@@ -566,6 +566,11 @@ Examples:
566
566
  `--no-solve` का उपयोग करें। विवरण के लिए
567
567
  [स्वचालित CI/CD सुधार](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation) देखें।
568
568
 
569
+ यदि पूरा `/fix` workflow उपलब्ध न हो, तो `/task --ci-cd <repository>` केवल वही CI/CD
570
+ issue-generation चरण चलाता है। यह बनाए गए issue का URL लौटाता है; सामान्य solve workflow
571
+ से remediation जारी रखने के लिए `/solve --development-log --deep-analysis --auto-merge`
572
+ से reply करें।
573
+
569
574
  #### `/limits` - उपयोग सीमाएँ दिखाएँ
570
575
 
571
576
  ```
package/README.md CHANGED
@@ -585,6 +585,11 @@ does not consume itself (e.g. `--tool`, `--model`, `--think`) is forwarded to
585
585
  [Automatic CI/CD Remediation](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)
586
586
  for details.
587
587
 
588
+ If the full `/fix` workflow is unavailable, `/task --ci-cd <repository>` runs
589
+ only the same CI/CD issue-generation step. It returns the created issue URL;
590
+ reply with `/solve --development-log --deep-analysis --auto-merge` to continue
591
+ the remediation through the normal solve workflow.
592
+
588
593
  #### `/limits` - Show Usage Limits
589
594
 
590
595
  ```
package/README.ru.md CHANGED
@@ -566,6 +566,11 @@ Examples:
566
566
  `/solve`. Подробнее см.
567
567
  [Автоматическое исправление CI/CD](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation).
568
568
 
569
+ Если полный процесс `/fix` недоступен, `/task --ci-cd <repository>` выполняет
570
+ только тот же этап создания issue для CI/CD. Команда возвращает URL созданного
571
+ issue; ответьте `/solve --development-log --deep-analysis --auto-merge`, чтобы
572
+ продолжить исправление обычным процессом solve.
573
+
569
574
  #### `/limits` — Показать лимиты использования
570
575
 
571
576
  ```
package/README.zh.md CHANGED
@@ -560,6 +560,10 @@ issue 交给 `/solve --development-log --deep-analysis --auto-merge` 处理。
560
560
  issue 的情况下预览,使用 `--no-solve` 可只创建 issue 而不启动 `/solve`。详见
561
561
  [自动 CI/CD 修复](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)。
562
562
 
563
+ 如果完整的 `/fix` 流程不可用,`/task --ci-cd <repository>` 只执行相同的 CI/CD issue
564
+ 生成步骤。该命令会返回新建 issue 的 URL;回复
565
+ `/solve --development-log --deep-analysis --auto-merge` 即可通过常规 solve 流程继续修复。
566
+
563
567
  #### `/limits` - 显示用量限制
564
568
 
565
569
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.10.5",
3
+ "version": "2.11.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Shared GitHub-backed CI/CD issue generation for `/fix --ci-cd` and
3
+ * `/task --ci-cd` (issues #1733 and #2121).
4
+ */
5
+
6
+ import { spawn } from 'child_process';
7
+ import { CI_CD_ISSUE_LABELS, CI_CD_ISSUE_TYPE, buildCiCdIssueBody, buildCiCdIssueTitle } from './fix.ci-cd.lib.mjs';
8
+ import { createTaskIssue } from './task.issue-creation.lib.mjs';
9
+
10
+ function runCommand(command, args, options = {}) {
11
+ return new Promise(resolve => {
12
+ const child = spawn(command, args, {
13
+ stdio: ['ignore', 'pipe', 'pipe'],
14
+ env: process.env,
15
+ ...options,
16
+ });
17
+ let stdout = '';
18
+ let stderr = '';
19
+ child.stdout.on('data', data => {
20
+ stdout += data.toString();
21
+ });
22
+ child.stderr.on('data', data => {
23
+ stderr += data.toString();
24
+ });
25
+ child.on('error', error => {
26
+ resolve({ code: 1, stdout, stderr: stderr || error.message });
27
+ });
28
+ child.on('close', code => {
29
+ resolve({ code, stdout, stderr });
30
+ });
31
+ });
32
+ }
33
+
34
+ async function commandOutput(run, command, args) {
35
+ const result = await run(command, args);
36
+ if (result.code !== 0) {
37
+ const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
38
+ throw new Error(output || `${command} exited with code ${result.code}`);
39
+ }
40
+ return result.stdout.trim();
41
+ }
42
+
43
+ async function detectLanguages(repository, run, warn) {
44
+ try {
45
+ const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/languages`]);
46
+ return JSON.parse(json);
47
+ } catch (error) {
48
+ warn(`⚠️ Could not detect languages: ${error.message}`);
49
+ return {};
50
+ }
51
+ }
52
+
53
+ async function getDefaultBranch(repository, run, warn) {
54
+ try {
55
+ return await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
56
+ } catch (error) {
57
+ warn(`⚠️ Could not determine default branch: ${error.message}`);
58
+ return null;
59
+ }
60
+ }
61
+
62
+ async function getLatestCommit(repository, branch, run, warn) {
63
+ if (!branch) return null;
64
+ try {
65
+ const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
66
+ return JSON.parse(json);
67
+ } catch (error) {
68
+ warn(`⚠️ Could not fetch latest commit: ${error.message}`);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
74
+
75
+ async function getRunsForCommit(repository, sha, run, warn) {
76
+ if (!sha) return [];
77
+ try {
78
+ const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
79
+ const parsed = JSON.parse(json);
80
+ return Array.isArray(parsed) ? parsed : [];
81
+ } catch (error) {
82
+ warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
83
+ return [];
84
+ }
85
+ }
86
+
87
+ async function getRecentBranchRuns(repository, branch, run, warn) {
88
+ if (!branch) return [];
89
+ try {
90
+ const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
91
+ const parsed = JSON.parse(json);
92
+ return Array.isArray(parsed) ? parsed : [];
93
+ } catch (error) {
94
+ warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
95
+ return [];
96
+ }
97
+ }
98
+
99
+ export async function prepareCiCdIssue({ repository, run = runCommand, warn = message => console.warn(message) }) {
100
+ const [languages, defaultBranch] = await Promise.all([detectLanguages(repository, run, warn), getDefaultBranch(repository, run, warn)]);
101
+ const commit = await getLatestCommit(repository, defaultBranch, run, warn);
102
+ let runs = await getRunsForCommit(repository, commit?.sha, run, warn);
103
+ let runsSource = 'commit';
104
+
105
+ if (runs.length === 0) {
106
+ const branchRuns = await getRecentBranchRuns(repository, defaultBranch, run, warn);
107
+ if (branchRuns.length > 0) {
108
+ runs = branchRuns;
109
+ runsSource = 'branch';
110
+ }
111
+ }
112
+
113
+ return {
114
+ repository,
115
+ defaultBranch,
116
+ commit,
117
+ runs,
118
+ languages,
119
+ runsSource,
120
+ title: buildCiCdIssueTitle(),
121
+ body: buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource }),
122
+ };
123
+ }
124
+
125
+ export async function createCiCdIssue({ repository, prepared = null, run = runCommand, log = null, warn = message => console.warn(message) }) {
126
+ const issueDraft = prepared || (await prepareCiCdIssue({ repository, run, warn }));
127
+ const issue = await createTaskIssue({
128
+ repository,
129
+ title: issueDraft.title,
130
+ body: issueDraft.body,
131
+ issueType: CI_CD_ISSUE_TYPE,
132
+ labels: [...CI_CD_ISSUE_LABELS],
133
+ run,
134
+ log,
135
+ });
136
+ return { ...issue, prepared: issueDraft };
137
+ }
package/src/fix.mjs CHANGED
@@ -16,7 +16,8 @@
16
16
  import path from 'path';
17
17
  import { spawn } from 'child_process';
18
18
  import { fileURLToPath } from 'url';
19
- import { CI_CD_ISSUE_LABELS, CI_CD_ISSUE_TYPE, buildCiCdIssueBody, buildCiCdIssueTitle, buildSolveArgs, partitionFixArgs, summarizeRunFailures } from './fix.ci-cd.lib.mjs';
19
+ import { buildSolveArgs, partitionFixArgs, summarizeRunFailures } from './fix.ci-cd.lib.mjs';
20
+ import { createCiCdIssue, prepareCiCdIssue } from './fix.ci-cd-issue.lib.mjs';
20
21
  import { setupStdioLogInterceptor } from './lib.mjs';
21
22
 
22
23
  setupStdioLogInterceptor();
@@ -44,95 +45,6 @@ Examples:
44
45
  fix.mjs owner/repo --ci-cd --think max --no-solve`);
45
46
  }
46
47
 
47
- function runCommand(command, args, options = {}) {
48
- return new Promise(resolve => {
49
- const child = spawn(command, args, {
50
- stdio: ['ignore', 'pipe', 'pipe'],
51
- env: process.env,
52
- ...options,
53
- });
54
- let stdout = '';
55
- let stderr = '';
56
- child.stdout.on('data', data => {
57
- stdout += data.toString();
58
- });
59
- child.stderr.on('data', data => {
60
- stderr += data.toString();
61
- });
62
- child.on('error', error => {
63
- resolve({ code: 1, stdout, stderr: stderr || error.message });
64
- });
65
- child.on('close', code => {
66
- resolve({ code, stdout, stderr });
67
- });
68
- });
69
- }
70
-
71
- async function commandOutput(command, args) {
72
- const result = await runCommand(command, args);
73
- if (result.code !== 0) {
74
- const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
75
- throw new Error(output || `${command} exited with code ${result.code}`);
76
- }
77
- return result.stdout.trim();
78
- }
79
-
80
- async function detectLanguages(repository) {
81
- try {
82
- const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/languages`]);
83
- return JSON.parse(json);
84
- } catch (error) {
85
- console.warn(`⚠️ Could not detect languages: ${error.message}`);
86
- return {};
87
- }
88
- }
89
-
90
- async function getDefaultBranch(repository) {
91
- try {
92
- return await commandOutput('gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
93
- } catch (error) {
94
- console.warn(`⚠️ Could not determine default branch: ${error.message}`);
95
- return null;
96
- }
97
- }
98
-
99
- async function getLatestCommit(repository, branch) {
100
- if (!branch) return null;
101
- try {
102
- const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
103
- return JSON.parse(json);
104
- } catch (error) {
105
- console.warn(`⚠️ Could not fetch latest commit: ${error.message}`);
106
- return null;
107
- }
108
- }
109
-
110
- const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
111
-
112
- async function getRunsForCommit(repository, sha) {
113
- if (!sha) return [];
114
- try {
115
- const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
116
- const parsed = JSON.parse(json);
117
- return Array.isArray(parsed) ? parsed : [];
118
- } catch (error) {
119
- console.warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
120
- return [];
121
- }
122
- }
123
-
124
- async function getRecentBranchRuns(repository, branch) {
125
- if (!branch) return [];
126
- try {
127
- const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
128
- const parsed = JSON.parse(json);
129
- return Array.isArray(parsed) ? parsed : [];
130
- } catch (error) {
131
- console.warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
132
- return [];
133
- }
134
- }
135
-
136
48
  function resolveSolveCommand() {
137
49
  return path.join(__dirname, 'solve.mjs');
138
50
  }
@@ -171,29 +83,14 @@ async function main() {
171
83
  const repository = parsed.repository;
172
84
  console.log(`🔧 /fix --ci-cd for ${repository.fullName}`);
173
85
 
174
- const [languages, defaultBranch] = await Promise.all([detectLanguages(repository), getDefaultBranch(repository)]);
175
- const commit = await getLatestCommit(repository, defaultBranch);
176
- let runs = await getRunsForCommit(repository, commit?.sha);
177
- let runsSource = 'commit';
178
-
179
- // Release/tag commits frequently produce no runs of their own. Fall back to
180
- // the most recent runs on the default branch so the issue stays actionable.
181
- if (runs.length === 0) {
182
- const branchRuns = await getRecentBranchRuns(repository, defaultBranch);
183
- if (branchRuns.length > 0) {
184
- runs = branchRuns;
185
- runsSource = 'branch';
186
- }
187
- }
86
+ const prepared = await prepareCiCdIssue({ repository });
87
+ const { defaultBranch, commit, runs, runsSource, title, body } = prepared;
188
88
 
189
89
  const { total, failing } = summarizeRunFailures(runs);
190
90
  console.log(` Default branch: ${defaultBranch || 'unknown'}`);
191
91
  console.log(` Latest commit: ${commit?.sha ? commit.sha.slice(0, 7) : 'unknown'}`);
192
92
  console.log(` CI/CD runs: ${total} (${failing} not passing)${runsSource === 'branch' ? ' [recent branch runs]' : ''}`);
193
93
 
194
- const title = buildCiCdIssueTitle();
195
- const body = buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource });
196
-
197
94
  if (parsed.dryRun) {
198
95
  console.log('\n--- DRY RUN: issue that would be created ---\n');
199
96
  console.log(`Title: ${title}\n`);
@@ -202,16 +99,9 @@ async function main() {
202
99
  }
203
100
 
204
101
  console.log('\n📝 Creating remediation issue...');
205
- const { createTaskIssue } = await import('./task.issue-creation.lib.mjs');
206
- const issue = await createTaskIssue({
102
+ const issue = await createCiCdIssue({
207
103
  repository,
208
- title,
209
- body,
210
- // The Bug type is what makes /solve --deep-analysis emit the root-cause
211
- // instructions this body omits (issue #1733).
212
- issueType: CI_CD_ISSUE_TYPE,
213
- labels: [...CI_CD_ISSUE_LABELS],
214
- run: runCommand,
104
+ prepared,
215
105
  log: message => console.log(message),
216
106
  });
217
107
  console.log(`✅ Created issue: ${issue.url}`);
@@ -556,9 +556,9 @@ en
556
556
  locked
557
557
  options "🔒 Locked options: `{{options}}`"
558
558
  task
559
- enabled "*/task* - Create a GitHub issue from a repository link and issue text"
560
- usage "Usage: `/task <github-repository-url>` followed by issue text, or reply with `/task`"
561
- example "Example: `/task https://github.com/owner/repo` then the issue text on following lines"
559
+ enabled "*/task* - Create a GitHub issue from text or CI/CD context"
560
+ usage "Usage: `/task <github-repository-url>` followed by issue text, or `/task --ci-cd <github-repository-url>`"
561
+ example "Example: `/task --ci-cd https://github.com/owner/repo` creates only the CI/CD remediation issue"
562
562
  disabled "*/task* / */split* - ❌ Disabled"
563
563
  split
564
564
  enabled "*/split* - Split a GitHub issue into smaller issues"
@@ -556,9 +556,9 @@ hi
556
556
  locked
557
557
  options "🔒 लॉक किए गए विकल्प: `{{options}}`"
558
558
  task
559
- enabled "*/task* - repository लिंक और issue text से GitHub issue बनाएँ"
560
- usage "उपयोग: `/task <github-repository-url>` के बाद issue text, या `/task` से reply करें"
561
- example "उदाहरण: `/task https://github.com/owner/repo`, फिर अगली पंक्तियों में issue text"
559
+ enabled "*/task* - text या CI/CD context से GitHub issue बनाएँ"
560
+ usage "उपयोग: `/task <github-repository-url>` के बाद issue text, या `/task --ci-cd <github-repository-url>`"
561
+ example "उदाहरण: `/task --ci-cd https://github.com/owner/repo` केवल CI/CD remediation issue बनाता है"
562
562
  disabled "*/task* / */split* - ❌ अक्षम"
563
563
  split
564
564
  enabled "*/split* - GitHub issue को छोटे issues में बाँटें"
@@ -556,9 +556,9 @@ ru
556
556
  locked
557
557
  options "🔒 Заблокированные опции: `{{options}}`"
558
558
  task
559
- enabled "*/task* - Создать задачу GitHub из ссылки на репозиторий и текста задачи"
560
- usage "Использование: `/task <github-repository-url>` с текстом задачи после команды или ответом `/task`"
561
- example "Пример: `/task https://github.com/owner/repo`, затем текст задачи на следующих строках"
559
+ enabled "*/task* - Создать issue GitHub из текста или контекста CI/CD"
560
+ usage "Использование: `/task <github-repository-url>` с текстом или `/task --ci-cd <github-repository-url>`"
561
+ example "Пример: `/task --ci-cd https://github.com/owner/repo` создаёт только issue для исправления CI/CD"
562
562
  disabled "*/task* / */split* - ❌ Отключено"
563
563
  split
564
564
  enabled "*/split* - Разделить задачу GitHub на меньшие задачи"
@@ -556,9 +556,9 @@ zh
556
556
  locked
557
557
  options "🔒 锁定选项:`{{options}}`"
558
558
  task
559
- enabled "*/task* - 根据仓库链接和 issue 文本创建 GitHub issue"
560
- usage "用法:`/task <github-repository-url>` 后接 issue 文本,或回复 `/task`"
561
- example "示例:`/task https://github.com/owner/repo`,然后在后续行写 issue 文本"
559
+ enabled "*/task* - 根据文本或 CI/CD 上下文创建 GitHub issue"
560
+ usage "用法:`/task <github-repository-url>` 后接 issue 文本,或 `/task --ci-cd <github-repository-url>`"
561
+ example "示例:`/task --ci-cd https://github.com/owner/repo` 只创建 CI/CD 修复 issue"
562
562
  disabled "*/task* / */split* - ❌ 已禁用"
563
563
  split
564
564
  enabled "*/split* - 将 GitHub issue 拆分为更小的 issue"
@@ -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);