@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.
- package/CHANGELOG.md +40 -0
- package/README.hi.md +22 -0
- package/README.md +24 -0
- package/README.ru.md +21 -0
- package/README.zh.md +20 -0
- package/package.json +3 -2
- package/src/fix.ci-cd.lib.mjs +480 -0
- package/src/fix.mjs +242 -0
- package/src/github-merge-ci-wait.lib.mjs +217 -0
- package/src/github-merge-ci.lib.mjs +10 -3
- package/src/github-merge-repo-actions.lib.mjs +14 -3
- package/src/github-merge.lib.mjs +13 -195
- package/src/interruptible-sleep.lib.mjs +34 -1
- package/src/locales/en.lino +6 -1
- package/src/locales/hi.lino +6 -1
- package/src/locales/ru.lino +6 -1
- package/src/locales/zh.lino +6 -1
- package/src/task.issue-creation.lib.mjs +31 -5
- package/src/telegram-bot.mjs +10 -85
- package/src/telegram-fix-command.lib.mjs +164 -0
- package/src/telegram-merge-queue.lib.mjs +23 -20
- package/src/telegram-merge-wait.lib.mjs +7 -1
- package/src/telegram-ui-messages.lib.mjs +11 -1
- package/src/telegram.config.lib.mjs +96 -0
package/src/github-merge.lib.mjs
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
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
|
-
|
|
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 };
|
package/src/locales/en.lino
CHANGED
|
@@ -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
|
package/src/locales/hi.lino
CHANGED
|
@@ -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
|
package/src/locales/ru.lino
CHANGED
|
@@ -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
|
package/src/locales/zh.lino
CHANGED
|
@@ -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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -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
|
|
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) {
|