@link-assistant/hive-mind 2.7.3 → 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.
@@ -20,6 +20,7 @@ import { parseGitHubUrl } from './github.lib.mjs';
20
20
  import { githubLimits } from './config.lib.mjs';
21
21
  import { ghWithRateLimitRetry } from './github-rate-limit.lib.mjs';
22
22
  import { getTerminalGitHubEntityErrorMessage, isTerminalGitHubEntityError } from './github-terminal-state.lib.mjs';
23
+ import { cancellableSleep } from './interruptible-sleep.lib.mjs';
23
24
 
24
25
  // Issue #1722: gh api `--paginate --slurp` responses for repos with many
25
26
  // historical workflow runs can easily exceed Node's default 1 MB exec buffer
@@ -49,6 +50,11 @@ export { syncReadyTags, getLinkedPRsFromTimeline, READY_LABEL };
49
50
  import { closeLinkedIssueIfNotAutoClosed } from './github-merge-issue-close.lib.mjs';
50
51
  export { closeLinkedIssueIfNotAutoClosed };
51
52
 
53
+ // Issue #2072: the long CI polling loops live in their own module (file size limit).
54
+ // Re-exported here so existing importers keep working.
55
+ import { waitForCI, waitForBranchCI } from './github-merge-ci-wait.lib.mjs';
56
+ export { waitForCI, waitForBranchCI };
57
+
52
58
  /**
53
59
  * Check if 'ready' label exists in repository
54
60
  * @param {string} owner - Repository owner
@@ -446,15 +452,19 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
446
452
  * @param {string} repo - Repository name
447
453
  * @param {number} prNumber - Pull request number
448
454
  * @param {boolean} verbose - Whether to log verbose output
449
- * @returns {Promise<{mergeable: boolean, mergeableState?: string|null, mergeStateStatus?: string|null, reason: string|null, terminal?: boolean}>}
455
+ * @param {Object} [options] - Extra options
456
+ * @param {Function} [options.isCancelled] - Issue #2072: polled during the retry delay so a cancel aborts the wait
457
+ * @returns {Promise<{mergeable: boolean, mergeableState?: string|null, mergeStateStatus?: string|null, reason: string|null, terminal?: boolean, cancelled?: boolean}>}
450
458
  */
451
- export async function checkPRMergeable(owner, repo, prNumber, verbose = false) {
459
+ export async function checkPRMergeable(owner, repo, prNumber, verbose = false, options = {}) {
460
+ const { isCancelled = null } = options;
452
461
  // Issue #1339: GitHub computes mergeability asynchronously. When mergeStateStatus is
453
462
  // 'UNKNOWN', it means GitHub hasn't calculated the merge state yet. Retry a few times.
454
463
  const MAX_UNKNOWN_RETRIES = 3;
455
464
  const UNKNOWN_RETRY_DELAY_MS = 5000;
456
465
 
457
466
  for (let attempt = 0; attempt < MAX_UNKNOWN_RETRIES; attempt++) {
467
+ if (isCancelled?.()) return { mergeable: false, reason: 'Operation was cancelled', cancelled: true };
458
468
  try {
459
469
  const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json mergeable,mergeStateStatus`);
460
470
  const pr = JSON.parse(stdout.trim());
@@ -466,7 +476,7 @@ export async function checkPRMergeable(owner, repo, prNumber, verbose = false) {
466
476
  if (verbose) {
467
477
  console.log(`[VERBOSE] /merge: PR #${prNumber} mergeability is UNKNOWN (attempt ${attempt + 1}/${MAX_UNKNOWN_RETRIES}), retrying in ${UNKNOWN_RETRY_DELAY_MS / 1000}s...`);
468
478
  }
469
- await new Promise(resolve => setTimeout(resolve, UNKNOWN_RETRY_DELAY_MS));
479
+ await cancellableSleep(UNKNOWN_RETRY_DELAY_MS, isCancelled);
470
480
  continue;
471
481
  }
472
482
  // All retries exhausted, still UNKNOWN - treat as not mergeable
@@ -604,95 +614,6 @@ export async function mergePullRequest(owner, repo, prNumber, options = {}, verb
604
614
  }
605
615
  }
606
616
 
607
- /**
608
- * Wait for CI/CD to complete with polling
609
- * @param {string} owner - Repository owner
610
- * @param {string} repo - Repository name
611
- * @param {number} prNumber - Pull request number
612
- * @param {Object} options - Wait options
613
- * @param {number} options.timeout - Maximum wait time in ms (default: 30 minutes)
614
- * @param {number} options.pollInterval - Polling interval in ms (default: 30 seconds)
615
- * @param {Function} options.onStatusUpdate - Callback for status updates
616
- * @param {boolean} verbose - Whether to log verbose output
617
- * @returns {Promise<{success: boolean, status: string, error: string|null}>}
618
- */
619
- export async function waitForCI(owner, repo, prNumber, options = {}, verbose = false) {
620
- const {
621
- timeout = 30 * 60 * 1000,
622
- pollInterval = 30 * 1000,
623
- onStatusUpdate = null,
624
- // Issue #1269: Add timeout for callback to prevent infinite blocking
625
- callbackTimeout = 60 * 1000, // 1 minute max for callback
626
- isCancelled = null, // Issue #1407: Support early exit when cancellation is requested
627
- } = options;
628
-
629
- const startTime = Date.now();
630
-
631
- while (Date.now() - startTime < timeout) {
632
- // Issue #1407: Check for cancellation before each poll to allow early exit
633
- if (isCancelled?.()) return { success: false, status: 'cancelled', error: 'Operation was cancelled' };
634
-
635
- let ciStatus;
636
- try {
637
- ciStatus = await checkPRCIStatus(owner, repo, prNumber, verbose);
638
- } catch (error) {
639
- // Issue #1269: Log and continue on CI check errors instead of crashing
640
- console.error(`[ERROR] /merge: Error checking CI status for PR #${prNumber}: ${error.message}`);
641
- verbose && console.error(`[VERBOSE] /merge: CI check error details:`, error);
642
- // Wait and retry
643
- await new Promise(resolve => setTimeout(resolve, pollInterval));
644
- continue;
645
- }
646
-
647
- if (onStatusUpdate) {
648
- // Issue #1269: Wrap callback with timeout to prevent infinite blocking; #1346: capture and clear timeout handle to prevent dangling timer
649
- try {
650
- let callbackTimeoutId;
651
- await Promise.race([
652
- onStatusUpdate(ciStatus),
653
- new Promise((_, reject) => {
654
- callbackTimeoutId = setTimeout(() => reject(new Error(`Callback timeout after ${callbackTimeout}ms`)), callbackTimeout);
655
- }),
656
- ]).finally(() => clearTimeout(callbackTimeoutId));
657
- } catch (callbackError) {
658
- // Issue #1269: Log callback errors but continue processing
659
- console.error(`[ERROR] /merge: Status update callback failed for PR #${prNumber}: ${callbackError.message}`);
660
- verbose && console.error(`[VERBOSE] /merge: Callback error details:`, callbackError);
661
- // Continue processing even if callback fails - don't let UI issues block merging
662
- }
663
- }
664
-
665
- if (ciStatus.status === 'success') {
666
- return { success: true, status: 'success', error: null };
667
- }
668
-
669
- if (ciStatus.status === 'failure') {
670
- return { success: false, status: 'failure', error: 'CI checks failed' };
671
- }
672
-
673
- if (ciStatus.status === 'terminal_github_entity_error') {
674
- return {
675
- success: false,
676
- status: 'terminal_github_entity_error',
677
- error: ciStatus.error || 'GitHub repository, pull request, issue, or branch is no longer accessible',
678
- };
679
- }
680
-
681
- if (ciStatus.status === 'pending') {
682
- if (verbose) {
683
- console.log(`[VERBOSE] /merge: Waiting for CI... (${Math.round((Date.now() - startTime) / 1000)}s elapsed)`);
684
- }
685
- await new Promise(resolve => setTimeout(resolve, pollInterval));
686
- continue;
687
- }
688
-
689
- // Unknown status - wait and retry
690
- await new Promise(resolve => setTimeout(resolve, pollInterval));
691
- }
692
-
693
- return { success: false, status: 'timeout', error: 'CI check timeout exceeded' };
694
- }
695
-
696
617
  /**
697
618
  * Parse and validate a repository URL for the merge command
698
619
  * @param {string} url - Repository URL
@@ -782,109 +703,6 @@ export async function getActiveBranchRuns(owner, repo, branch = 'main', verbose
782
703
  };
783
704
  }
784
705
 
785
- /**
786
- * Wait for all active workflow runs on a branch to complete
787
- * Issue #1307: Ensures all CI runs on target branch are complete before merging
788
- * @param {string} owner - Repository owner
789
- * @param {string} repo - Repository name
790
- * @param {string} branch - Branch name (default: main)
791
- * @param {Object} options - Wait options
792
- * @param {number} options.timeout - Maximum wait time in ms (default: 45 minutes)
793
- * @param {number} options.pollInterval - Polling interval in ms (default: 30 seconds)
794
- * @param {Function} options.onStatusUpdate - Callback for status updates
795
- * @param {boolean} verbose - Whether to log verbose output
796
- * @returns {Promise<{success: boolean, waitedForRuns: boolean, completedRuns: number, error: string|null}>}
797
- */
798
- export async function waitForBranchCI(owner, repo, branch = 'main', options = {}, verbose = false) {
799
- const { timeout = 45 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null, isCancelled = null } = options;
800
-
801
- const startTime = Date.now();
802
- let totalWaitedRuns = 0;
803
-
804
- if (verbose) {
805
- console.log(`[VERBOSE] /merge: Checking for active CI runs on ${owner}/${repo} branch ${branch}...`);
806
- }
807
-
808
- while (Date.now() - startTime < timeout) {
809
- if (isCancelled?.()) return { success: false, waitedForRuns: totalWaitedRuns > 0, completedRuns: totalWaitedRuns, error: 'Operation was cancelled' };
810
- let activeRuns;
811
- try {
812
- activeRuns = await getActiveBranchRuns(owner, repo, branch, verbose);
813
- } catch (error) {
814
- // Log and continue on errors
815
- console.error(`[ERROR] /merge: Error checking branch CI: ${error.message}`);
816
- await new Promise(resolve => setTimeout(resolve, pollInterval));
817
- continue;
818
- }
819
-
820
- if (onStatusUpdate) {
821
- try {
822
- await onStatusUpdate({
823
- hasActiveRuns: activeRuns.hasActiveRuns,
824
- count: activeRuns.count,
825
- runs: activeRuns.runs,
826
- elapsedMs: Date.now() - startTime,
827
- });
828
- } catch (callbackError) {
829
- // Log callback errors but continue
830
- console.error(`[ERROR] /merge: Status update callback failed: ${callbackError.message}`);
831
- }
832
- }
833
-
834
- if (!activeRuns.hasActiveRuns) {
835
- if (verbose) {
836
- console.log(`[VERBOSE] /merge: No active CI runs on ${branch} branch. Ready to proceed.`);
837
- }
838
- return {
839
- success: true,
840
- waitedForRuns: totalWaitedRuns > 0,
841
- completedRuns: totalWaitedRuns,
842
- error: null,
843
- };
844
- }
845
-
846
- totalWaitedRuns = Math.max(totalWaitedRuns, activeRuns.count);
847
-
848
- if (verbose) {
849
- const elapsedSec = Math.round((Date.now() - startTime) / 1000);
850
- console.log(`[VERBOSE] /merge: Waiting for ${activeRuns.count} active runs on ${branch}... (${elapsedSec}s elapsed)`);
851
- }
852
-
853
- await new Promise(resolve => setTimeout(resolve, pollInterval));
854
- }
855
-
856
- // Timeout reached
857
- // Issue #1722: if the final check throws, do NOT silently report "ready".
858
- // Treat it the same as still-active (force a timeout failure), so /merge
859
- // waits/retries instead of merging on top of a still-running CI run.
860
- let finalCheck;
861
- try {
862
- finalCheck = await getActiveBranchRuns(owner, repo, branch, verbose);
863
- } catch (error) {
864
- return {
865
- success: false,
866
- waitedForRuns: true,
867
- completedRuns: totalWaitedRuns,
868
- error: `Timeout reached and final CI check failed on ${branch}: ${error.message}`,
869
- };
870
- }
871
- if (finalCheck.hasActiveRuns) {
872
- return {
873
- success: false,
874
- waitedForRuns: true,
875
- completedRuns: totalWaitedRuns - finalCheck.count,
876
- error: `Timeout waiting for ${finalCheck.count} CI runs on ${branch} branch`,
877
- };
878
- }
879
-
880
- return {
881
- success: true,
882
- waitedForRuns: totalWaitedRuns > 0,
883
- completedRuns: totalWaitedRuns,
884
- error: null,
885
- };
886
- }
887
-
888
706
  /**
889
707
  * Get the default branch for a repository
890
708
  * @param {string} owner - Repository owner
@@ -49,4 +49,37 @@ export function interruptibleSleep(ms) {
49
49
  });
50
50
  }
51
51
 
52
- export default { interruptibleSleep };
52
+ /**
53
+ * Sleep for `ms` milliseconds, resolving early if SIGINT/SIGTERM arrives or if
54
+ * `isCancelled()` starts returning true.
55
+ *
56
+ * Issue #2072: polling loops used to `await new Promise(r => setTimeout(r, pollInterval))`
57
+ * and only re-check cancellation on the next iteration. With a 30s poll interval that
58
+ * made `/merge` keep running for up to a full interval after the Cancel button was
59
+ * pressed. Sleeping in short steps lets cancellation take effect within `stepMs`.
60
+ *
61
+ * @param {number} ms - Duration in milliseconds
62
+ * @param {Function|null} isCancelled - Predicate polled during the sleep
63
+ * @param {Object} [options]
64
+ * @param {number} [options.stepMs=100] - Granularity at which `isCancelled` is polled
65
+ * @returns {Promise<{interrupted: boolean, cancelled: boolean}>}
66
+ */
67
+ export async function cancellableSleep(ms, isCancelled = null, options = {}) {
68
+ const { stepMs = 100 } = options;
69
+
70
+ if (!isCancelled) {
71
+ const { interrupted } = await interruptibleSleep(ms);
72
+ return { interrupted, cancelled: false };
73
+ }
74
+
75
+ const deadline = Date.now() + ms;
76
+ while (Date.now() < deadline) {
77
+ if (isCancelled()) return { interrupted: false, cancelled: true };
78
+ const { interrupted } = await interruptibleSleep(Math.min(stepMs, deadline - Date.now()));
79
+ if (interrupted) return { interrupted: true, cancelled: isCancelled() };
80
+ }
81
+
82
+ return { interrupted: false, cancelled: isCancelled() };
83
+ }
84
+
85
+ export default { interruptibleSleep, cancellableSleep };
@@ -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) {