@link-assistant/hive-mind 2.7.4 → 2.8.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/src/fix.mjs ADDED
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * `/fix` command (issue #1733).
5
+ *
6
+ * Currently implements `--ci-cd`: automatically generate a CI/CD remediation
7
+ * issue for a target repository and (optionally) hand it off to
8
+ * `/solve --development-log --deep-analysis --auto-merge`.
9
+ *
10
+ * fix.mjs <github-repository-url> --ci-cd [solve options...]
11
+ *
12
+ * Every option `/fix` does not consume itself (e.g. --tool, --model, --think)
13
+ * is forwarded to `/solve`.
14
+ */
15
+
16
+ import path from 'path';
17
+ import { spawn } from 'child_process';
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';
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ function printHelp() {
24
+ console.log(`Usage: fix.mjs <github-repository-url> --ci-cd [options]
25
+
26
+ Automatically generate a CI/CD remediation issue for a repository and hand it
27
+ off to /solve --development-log --deep-analysis --auto-merge.
28
+
29
+ Options:
30
+ --ci-cd Generate a CI/CD remediation issue (required mode)
31
+ --dry-run Print the issue that would be created without creating it
32
+ --no-solve Create the issue but do not start /solve on it
33
+ --version Show version number
34
+ --help, -h Show help
35
+
36
+ All other options (e.g. --tool, --model, --think) are forwarded to /solve.
37
+
38
+ Examples:
39
+ fix.mjs https://github.com/owner/repo --ci-cd
40
+ fix.mjs https://github.com/owner/repo --ci-cd --tool codex --model gpt-5.5
41
+ fix.mjs owner/repo --ci-cd --think max --no-solve`);
42
+ }
43
+
44
+ function runCommand(command, args, options = {}) {
45
+ return new Promise(resolve => {
46
+ const child = spawn(command, args, {
47
+ stdio: ['ignore', 'pipe', 'pipe'],
48
+ env: process.env,
49
+ ...options,
50
+ });
51
+ let stdout = '';
52
+ let stderr = '';
53
+ child.stdout.on('data', data => {
54
+ stdout += data.toString();
55
+ });
56
+ child.stderr.on('data', data => {
57
+ stderr += data.toString();
58
+ });
59
+ child.on('error', error => {
60
+ resolve({ code: 1, stdout, stderr: stderr || error.message });
61
+ });
62
+ child.on('close', code => {
63
+ resolve({ code, stdout, stderr });
64
+ });
65
+ });
66
+ }
67
+
68
+ async function commandOutput(command, args) {
69
+ const result = await runCommand(command, args);
70
+ if (result.code !== 0) {
71
+ const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
72
+ throw new Error(output || `${command} exited with code ${result.code}`);
73
+ }
74
+ return result.stdout.trim();
75
+ }
76
+
77
+ async function detectLanguages(repository) {
78
+ try {
79
+ const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/languages`]);
80
+ return JSON.parse(json);
81
+ } catch (error) {
82
+ console.warn(`⚠️ Could not detect languages: ${error.message}`);
83
+ return {};
84
+ }
85
+ }
86
+
87
+ async function getDefaultBranch(repository) {
88
+ try {
89
+ return await commandOutput('gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
90
+ } catch (error) {
91
+ console.warn(`⚠️ Could not determine default branch: ${error.message}`);
92
+ return null;
93
+ }
94
+ }
95
+
96
+ async function getLatestCommit(repository, branch) {
97
+ if (!branch) return null;
98
+ try {
99
+ const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
100
+ return JSON.parse(json);
101
+ } catch (error) {
102
+ console.warn(`⚠️ Could not fetch latest commit: ${error.message}`);
103
+ return null;
104
+ }
105
+ }
106
+
107
+ const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
108
+
109
+ async function getRunsForCommit(repository, sha) {
110
+ if (!sha) return [];
111
+ try {
112
+ const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
113
+ const parsed = JSON.parse(json);
114
+ return Array.isArray(parsed) ? parsed : [];
115
+ } catch (error) {
116
+ console.warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
117
+ return [];
118
+ }
119
+ }
120
+
121
+ async function getRecentBranchRuns(repository, branch) {
122
+ if (!branch) return [];
123
+ try {
124
+ const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
125
+ const parsed = JSON.parse(json);
126
+ return Array.isArray(parsed) ? parsed : [];
127
+ } catch (error) {
128
+ console.warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
129
+ return [];
130
+ }
131
+ }
132
+
133
+ function resolveSolveCommand() {
134
+ return path.join(__dirname, 'solve.mjs');
135
+ }
136
+
137
+ async function main() {
138
+ const rawArgs = process.argv.slice(2);
139
+
140
+ if (rawArgs.includes('--version')) {
141
+ const { getVersion } = await import('./version.lib.mjs');
142
+ try {
143
+ console.log(await getVersion());
144
+ } catch {
145
+ console.error('Error: Unable to determine version');
146
+ process.exit(1);
147
+ }
148
+ return;
149
+ }
150
+
151
+ if (rawArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) {
152
+ printHelp();
153
+ process.exit(rawArgs.length === 0 ? 1 : 0);
154
+ }
155
+
156
+ const parsed = partitionFixArgs(rawArgs);
157
+
158
+ if (!parsed.ciCd) {
159
+ console.error('❌ /fix currently supports only --ci-cd mode. Pass --ci-cd to continue.');
160
+ process.exit(1);
161
+ }
162
+
163
+ if (!parsed.repository) {
164
+ console.error('❌ Missing or invalid GitHub repository URL. Provide it as the first argument, e.g. fix.mjs https://github.com/owner/repo --ci-cd');
165
+ process.exit(1);
166
+ }
167
+
168
+ const repository = parsed.repository;
169
+ console.log(`🔧 /fix --ci-cd for ${repository.fullName}`);
170
+
171
+ const [languages, defaultBranch] = await Promise.all([detectLanguages(repository), getDefaultBranch(repository)]);
172
+ const commit = await getLatestCommit(repository, defaultBranch);
173
+ let runs = await getRunsForCommit(repository, commit?.sha);
174
+ let runsSource = 'commit';
175
+
176
+ // Release/tag commits frequently produce no runs of their own. Fall back to
177
+ // the most recent runs on the default branch so the issue stays actionable.
178
+ if (runs.length === 0) {
179
+ const branchRuns = await getRecentBranchRuns(repository, defaultBranch);
180
+ if (branchRuns.length > 0) {
181
+ runs = branchRuns;
182
+ runsSource = 'branch';
183
+ }
184
+ }
185
+
186
+ const { total, failing } = summarizeRunFailures(runs);
187
+ console.log(` Default branch: ${defaultBranch || 'unknown'}`);
188
+ console.log(` Latest commit: ${commit?.sha ? commit.sha.slice(0, 7) : 'unknown'}`);
189
+ console.log(` CI/CD runs: ${total} (${failing} not passing)${runsSource === 'branch' ? ' [recent branch runs]' : ''}`);
190
+
191
+ const title = buildCiCdIssueTitle();
192
+ const body = buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource });
193
+
194
+ if (parsed.dryRun) {
195
+ console.log('\n--- DRY RUN: issue that would be created ---\n');
196
+ console.log(`Title: ${title}\n`);
197
+ console.log(body);
198
+ return;
199
+ }
200
+
201
+ console.log('\n📝 Creating remediation issue...');
202
+ const { createTaskIssue } = await import('./task.issue-creation.lib.mjs');
203
+ const issue = await createTaskIssue({
204
+ repository,
205
+ title,
206
+ body,
207
+ // The Bug type is what makes /solve --deep-analysis emit the root-cause
208
+ // instructions this body omits (issue #1733).
209
+ issueType: CI_CD_ISSUE_TYPE,
210
+ labels: [...CI_CD_ISSUE_LABELS],
211
+ run: runCommand,
212
+ log: message => console.log(message),
213
+ });
214
+ console.log(`✅ Created issue: ${issue.url}`);
215
+
216
+ if (!parsed.runSolve) {
217
+ console.log('ℹ️ --no-solve set; skipping /solve. Run it manually with:');
218
+ console.log(` solve ${buildSolveArgs({ issueUrl: issue.url, passthrough: parsed.passthrough }).join(' ')}`);
219
+ return;
220
+ }
221
+
222
+ const solveArgs = buildSolveArgs({ issueUrl: issue.url, passthrough: parsed.passthrough });
223
+ const solveCommand = resolveSolveCommand();
224
+ console.log(`\n🚀 Starting /solve: solve ${solveArgs.join(' ')}`);
225
+
226
+ await new Promise((resolve, reject) => {
227
+ const child = spawn(process.execPath, [solveCommand, ...solveArgs], {
228
+ stdio: 'inherit',
229
+ env: process.env,
230
+ });
231
+ child.on('error', reject);
232
+ child.on('close', code => {
233
+ if (code === 0) resolve();
234
+ else reject(new Error(`solve exited with code ${code}`));
235
+ });
236
+ });
237
+ }
238
+
239
+ main().catch(error => {
240
+ console.error(`❌ ${error.message}`);
241
+ process.exit(1);
242
+ });
@@ -564,6 +564,11 @@ en
564
564
  enabled "*/split* - Split a GitHub issue into smaller issues"
565
565
  usage "Usage: `/split <github-issue-url> [options]` or `/task --split <github-issue-url>`"
566
566
  example "Example: `/split https://github.com/owner/repo/issues/123 --split-count 2`"
567
+ fix
568
+ enabled "*/fix* - Create a CI/CD remediation issue for a repository and solve it"
569
+ usage "Usage: `/fix <github-repository-url> [options]`"
570
+ example "Example: `/fix https://github.com/owner/repo --model sonnet`"
571
+ disabled "*/fix* - ❌ Disabled"
567
572
  hive
568
573
  enabled "*/hive* - Run hive command"
569
574
  usage "Usage: `/hive <github-url> [options]`"
@@ -590,7 +595,7 @@ en
590
595
  isolation
591
596
  mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
592
597
  group
593
- note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
598
+ note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
594
599
  common
595
600
  options "🔧 *Common Options:*"
596
601
  model
@@ -564,6 +564,11 @@ hi
564
564
  enabled "*/split* - GitHub issue को छोटे issues में बाँटें"
565
565
  usage "उपयोग: `/split <github-issue-url> [options]` या `/task --split <github-issue-url>`"
566
566
  example "उदाहरण: `/split https://github.com/owner/repo/issues/123 --split-count 2`"
567
+ fix
568
+ enabled "*/fix* - repository के लिए CI/CD सुधार issue बनाएँ और उसे हल करें"
569
+ usage "उपयोग: `/fix <github-repository-url> [options]`"
570
+ example "उदाहरण: `/fix https://github.com/owner/repo --model sonnet`"
571
+ disabled "*/fix* - ❌ अक्षम"
567
572
  hive
568
573
  enabled "*/hive* - hive command चलाएँ"
569
574
  usage "उपयोग: `/hive <github-url> [options]`"
@@ -590,7 +595,7 @@ hi
590
595
  isolation
591
596
  mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
592
597
  group
593
- note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
598
+ note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
594
599
  common
595
600
  options "🔧 *Common Options:*"
596
601
  model
@@ -564,6 +564,11 @@ ru
564
564
  enabled "*/split* - Разделить задачу GitHub на меньшие задачи"
565
565
  usage "Использование: `/split <github-issue-url> [options]` или `/task --split <github-issue-url>`"
566
566
  example "Пример: `/split https://github.com/owner/repo/issues/123 --split-count 2`"
567
+ fix
568
+ enabled "*/fix* - Создать задачу об исправлении CI/CD для репозитория и решить её"
569
+ usage "Использование: `/fix <github-repository-url> [options]`"
570
+ example "Пример: `/fix https://github.com/owner/repo --model sonnet`"
571
+ disabled "*/fix* - ❌ Отключено"
567
572
  hive
568
573
  enabled "*/hive* - Выполнить команду hive"
569
574
  usage "Использование: `/hive <github-url> [options]`"
@@ -590,7 +595,7 @@ ru
590
595
  isolation
591
596
  mode "🔒 *Режим изоляции:* `{{isolationBackend}}` (экспериментально)"
592
597
  group
593
- note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
598
+ note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
594
599
  common
595
600
  options "🔧 *Общие опции:*"
596
601
  model
@@ -564,6 +564,11 @@ zh
564
564
  enabled "*/split* - 将 GitHub issue 拆分为更小的 issue"
565
565
  usage "用法:`/split <github-issue-url> [options]` 或 `/task --split <github-issue-url>`"
566
566
  example "示例:`/split https://github.com/owner/repo/issues/123 --split-count 2`"
567
+ fix
568
+ enabled "*/fix* - 为仓库创建 CI/CD 修复 issue 并解决它"
569
+ usage "用法:`/fix <github-repository-url> [options]`"
570
+ example "示例:`/fix https://github.com/owner/repo --model sonnet`"
571
+ disabled "*/fix* - ❌ 已禁用"
567
572
  hive
568
573
  enabled "*/hive* - 运行 hive 命令"
569
574
  usage "用法:`/hive <github-url> [options]`"
@@ -590,7 +595,7 @@ zh
590
595
  isolation
591
596
  mode "🔒 *隔离模式:* `{{isolationBackend}}`(实验性)"
592
597
  group
593
- note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/hive、/queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
598
+ note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/fix、/hive、/queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
594
599
  common
595
600
  options "🔧 *常用选项:*"
596
601
  model
@@ -196,18 +196,44 @@ export function parseCreatedTaskIssueOutput(output) {
196
196
  throw new Error(`Could not parse created issue URL from gh output: ${String(output || '').trim()}`);
197
197
  }
198
198
 
199
- export async function createTaskIssue({ repository, title, body, run = runCommand }) {
199
+ export function buildCreateIssueArgs({ repository, title, bodyFile, issueType = null, labels = [] }) {
200
+ const args = ['issue', 'create', '--repo', repository.fullName, '--title', title, '--body-file', bodyFile];
201
+ if (issueType) args.push('--type', issueType);
202
+ for (const label of labels) args.push('--label', label);
203
+ return args;
204
+ }
205
+
206
+ /**
207
+ * Create an issue via `gh issue create`.
208
+ *
209
+ * `issueType` and `labels` are optional and best-effort: issue types are
210
+ * configured per organization and labels per repository, so a target repo may
211
+ * not have them. Rather than failing the whole command, a rejected create is
212
+ * retried once without them (issue #1733).
213
+ */
214
+ export async function createTaskIssue({ repository, title, body, issueType = null, labels = [], run = runCommand, log = null }) {
200
215
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hive-mind-task-issue-'));
201
216
  const bodyFile = path.join(tempDir, 'body.md');
202
217
 
203
218
  try {
204
219
  await fs.writeFile(bodyFile, body);
205
- const result = await run('gh', ['issue', 'create', '--repo', repository.fullName, '--title', title, '--body-file', bodyFile]);
206
- if (result.code !== 0) {
207
- const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
220
+
221
+ const result = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile, issueType, labels }));
222
+ if (result.code === 0) return parseCreatedTaskIssueOutput(result.stdout);
223
+
224
+ const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
225
+ const usedOptionalMetadata = Boolean(issueType) || labels.length > 0;
226
+ if (!usedOptionalMetadata) {
208
227
  throw new Error(output || `gh issue create exited with code ${result.code}`);
209
228
  }
210
- return parseCreatedTaskIssueOutput(result.stdout);
229
+
230
+ await log?.(`⚠️ Could not create issue with type/labels (${output || `exit code ${result.code}`}); retrying without them`);
231
+ const retry = await run('gh', buildCreateIssueArgs({ repository, title, bodyFile }));
232
+ if (retry.code !== 0) {
233
+ const retryOutput = `${retry.stderr || ''}${retry.stdout || ''}`.trim();
234
+ throw new Error(retryOutput || `gh issue create exited with code ${retry.code}`);
235
+ }
236
+ return parseCreatedTaskIssueOutput(retry.stdout);
211
237
  } finally {
212
238
  await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
213
239
  }
@@ -23,94 +23,14 @@ dotenvx.config({ quiet: true, ignore: ['MISSING_ENV_FILE'] });
23
23
  await loadLenvConfig({ override: true, quiet: true });
24
24
 
25
25
  const yargs = getLinoYargsFactory();
26
+ const { createYargsConfig: createTelegramYargsConfig } = await import('./telegram.config.lib.mjs');
26
27
  const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
27
28
  const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
28
29
  const { enhanceUnknownArgumentError } = await import('./option-suggestions.lib.mjs');
29
30
  const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
30
31
  const { extractIsolationFromArgs, isValidPerCommandIsolation } = await import('./telegram-isolation.lib.mjs');
31
32
 
32
- const config = yargs(hideBin(process.argv))
33
- .usage('Usage: hive-telegram-bot [options]')
34
- .option('configuration', {
35
- type: 'string',
36
- description: 'LINO configuration string for environment variables',
37
- alias: 'c',
38
- default: getenv('TELEGRAM_CONFIGURATION', ''),
39
- })
40
- .option('token', {
41
- type: 'string',
42
- description: 'Telegram bot token from @BotFather',
43
- alias: 't',
44
- default: getenv('TELEGRAM_BOT_TOKEN', ''),
45
- })
46
- .option('allowedChats', {
47
- type: 'string',
48
- description: 'Allowed chat IDs in lino notation, e.g., "(\n 123456789\n 987654321\n)"',
49
- alias: 'allowed-chats',
50
- default: getenv('TELEGRAM_ALLOWED_CHATS', ''),
51
- })
52
- .option('allowedTopics', {
53
- type: 'string',
54
- description: 'Allowed topic IDs in Links Notation format "chatId topicId" pairs',
55
- alias: 'allowed-topics',
56
- default: getenv('TELEGRAM_ALLOWED_TOPICS', ''),
57
- })
58
- .option('solveOverrides', {
59
- type: 'string',
60
- description: 'Override options for /solve command in lino notation, e.g., "(\n --auto-continue\n --attach-logs\n)"',
61
- alias: 'solve-overrides',
62
- default: getenv('TELEGRAM_SOLVE_OVERRIDES', ''),
63
- })
64
- .option('hiveOverrides', {
65
- type: 'string',
66
- description: 'Override options for /hive command in lino notation, e.g., "(\n --verbose\n --all-issues\n)"',
67
- alias: 'hive-overrides',
68
- default: getenv('TELEGRAM_HIVE_OVERRIDES', ''),
69
- })
70
- .option('solve', {
71
- type: 'boolean',
72
- description: 'Enable /solve command (use --no-solve to disable)',
73
- default: getenv('TELEGRAM_SOLVE', 'true') !== 'false',
74
- })
75
- .option('hive', {
76
- type: 'boolean',
77
- description: 'Enable /hive command (use --no-hive to disable)',
78
- default: getenv('TELEGRAM_HIVE', 'true') !== 'false',
79
- })
80
- .option('task', {
81
- type: 'boolean',
82
- description: 'Enable /task and /split commands (use --no-task to disable)',
83
- default: getenv('TELEGRAM_TASK', 'true') !== 'false',
84
- })
85
- .option('auth', {
86
- type: 'boolean',
87
- description: 'Enable experimental private /auth command for allowlisted chat owners (use --no-auth to disable)',
88
- default: getenv('TELEGRAM_AUTH', 'true') !== 'false',
89
- })
90
- .option('dryRun', {
91
- type: 'boolean',
92
- description: 'Validate configuration and options without starting the bot',
93
- alias: 'dry-run',
94
- default: false,
95
- })
96
- .option('verbose', {
97
- type: 'boolean',
98
- description: 'Enable verbose logging for debugging',
99
- alias: 'v',
100
- default: getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true',
101
- })
102
- .option('autoStartScreenWatchMessage', { type: 'boolean', description: 'Experimental: auto-start separate /terminal_watch messages for public /solve sessions', alias: 'auto-start-screen-watch-message', default: getenv('TELEGRAM_AUTO_START_SCREEN_WATCH_MESSAGE', getenv('TELEGRAM_AUTO_WATCH_MESSAGE', 'false')) === 'true' })
103
- // Issue #594: bot-owner toggle for --show-limits virtual option in /solve and /hive.
104
- .option('showLimits', { type: 'boolean', description: 'Experimental: allow /solve and /hive callers to use --show-limits to embed Claude/Codex usage at start, end, and delta in the completion message', alias: 'show-limits', default: getenv('TELEGRAM_SHOW_LIMITS', 'true') !== 'false' })
105
- .option('isolation', { type: 'string', description: "Isolation backend (screen/tmux/docker). Defaults to 'docker' so Telegram-bot work sessions run in Docker isolation; pass --isolation '' (or set TELEGRAM_ISOLATION='') to disable.", default: getenv('TELEGRAM_ISOLATION', 'docker') })
106
- .help('h')
107
- .alias('h', 'help')
108
- .parserConfiguration({
109
- 'boolean-negation': true,
110
- 'strip-dashed': true, // Remove dashed keys from argv to simplify validation
111
- })
112
- .strict() // Enable strict mode to reject unknown options (consistent with solve.mjs and hive.mjs)
113
- .parse();
33
+ const config = createTelegramYargsConfig(yargs(hideBin(process.argv))).parse();
114
34
 
115
35
  // Configuration priority: CLI option > --configuration LINO > .lenv > .env
116
36
  if (config.configuration) {
@@ -150,6 +70,7 @@ const hiveOverrides = resolvedHiveOverrides
150
70
  const solveEnabled = config.solve;
151
71
  const hiveEnabled = config.hive;
152
72
  const taskEnabled = config.task;
73
+ const fixEnabled = config.fix;
153
74
  const authEnabled = config.auth;
154
75
  // Isolation mode (experimental): uses `$` from start-command with specified backend
155
76
  const ISOLATION_BACKEND = (config.isolation || getenv('TELEGRAM_ISOLATION', '')).trim().toLowerCase();
@@ -299,7 +220,7 @@ if (config.dryRun) {
299
220
  if (allowedTopics && allowedTopics.length > 0) {
300
221
  console.log(' Allowed topics:', lino.formatLinks(allowedTopics));
301
222
  }
302
- console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
223
+ console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, fix: fixEnabled, auth: authEnabled });
303
224
  if (solveOverrides.length > 0) {
304
225
  console.log(' Solve overrides:', lino.format(solveOverrides));
305
226
  }
@@ -570,6 +491,7 @@ bot.command('help', async ctx => {
570
491
  stopReason: stopInfo?.reason || DEFAULT_STOP_REASON,
571
492
  solveEnabled,
572
493
  taskEnabled,
494
+ fixEnabled,
573
495
  hiveEnabled,
574
496
  solveOverrides,
575
497
  hiveOverrides,
@@ -672,6 +594,8 @@ const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.m
672
594
  registerSubscribeCommands(bot, sharedCommandOpts);
673
595
  const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
674
596
  const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
597
+ const { registerFixCommand } = await import('./telegram-fix-command.lib.mjs');
598
+ const { handleFixCommand, FIX_COMMAND_NAMES } = registerFixCommand(bot, { ...sharedCommandOpts, fixEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
675
599
  const { registerAuthCommand } = await import('./telegram-auth-command.lib.mjs');
676
600
  const { handleAuthCommand } = registerAuthCommand(bot, { ...sharedCommandOpts, allowedChats, authEnabled, safeReply });
677
601
 
@@ -1235,7 +1159,8 @@ bot.on('message', async (ctx, next) => {
1235
1159
  // /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback — Telegraf's bot.command() is sufficient.
1236
1160
  const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
1237
1161
  const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
1238
- const handlers = { ...solveHandlers, ...taskHandlers, auth: handleAuthCommand, hive: handleHiveCommand, queue: handleSolveQueueCommand };
1162
+ const fixHandlers = Object.fromEntries(FIX_COMMAND_NAMES.map(command => [command, handleFixCommand]));
1163
+ const handlers = { ...solveHandlers, ...taskHandlers, ...fixHandlers, auth: handleAuthCommand, hive: handleHiveCommand, queue: handleSolveQueueCommand };
1239
1164
 
1240
1165
  const handler = handlers[extracted.command];
1241
1166
  if (!handler) return next();
@@ -1344,7 +1269,7 @@ if (allowedChats && allowedChats.length > 0) {
1344
1269
  if (allowedTopics && allowedTopics.length > 0) {
1345
1270
  console.log('Allowed topics (lino):', lino.formatLinks(allowedTopics));
1346
1271
  }
1347
- console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
1272
+ console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, fix: fixEnabled, auth: authEnabled });
1348
1273
  if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
1349
1274
  if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1350
1275
  if (VERBOSE) {