@link-assistant/hive-mind 1.30.5 → 1.31.1
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 +73 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +3 -0
- package/src/agent.prompts.lib.mjs +6 -1
- package/src/claude.lib.mjs +4 -1
- package/src/claude.prompts.lib.mjs +6 -1
- package/src/codex.lib.mjs +2 -0
- package/src/codex.prompts.lib.mjs +6 -1
- package/src/exit-handler.lib.mjs +16 -1
- package/src/github-merge-ready-sync.lib.mjs +251 -0
- package/src/github-merge.lib.mjs +15 -185
- package/src/opencode.lib.mjs +2 -0
- package/src/opencode.prompts.lib.mjs +6 -1
- package/src/option-suggestions.lib.mjs +3 -0
- package/src/solve.auto-ensure.lib.mjs +120 -0
- package/src/solve.auto-merge.lib.mjs +61 -5
- package/src/solve.config.lib.mjs +26 -0
- package/src/solve.error-handlers.lib.mjs +39 -0
- package/src/solve.interrupt.lib.mjs +70 -0
- package/src/solve.mjs +39 -61
- package/src/telegram-merge-command.lib.mjs +23 -1
- package/src/telegram-merge-queue.lib.mjs +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.31.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5108367: fix: fix root causes of 20-32h process hang after session ends (Issue #1335)
|
|
8
|
+
|
|
9
|
+
Two separate bugs caused `solve` processes to run for 20–32 hours after work was complete:
|
|
10
|
+
|
|
11
|
+
**Bug A — Infinite loop for repos without CI:** When `--auto-restart-until-mergeable` is used
|
|
12
|
+
on a repository with no CI/CD workflows, the `watchUntilMergeable` loop was permanently stuck
|
|
13
|
+
on "CI/CD checks have not started yet" with no exit condition. The root cause was that the code
|
|
14
|
+
treated `no_checks` identically for both transient race conditions (CI hasn't started yet after
|
|
15
|
+
a push) and permanent states (repo has no CI at all). Fixed by checking whether the repository
|
|
16
|
+
actually has GitHub Actions workflows configured (`hasRepoWorkflows()`). If none exist, the
|
|
17
|
+
`no_checks` state is permanent and the monitor exits immediately, treating the PR as CI-passing.
|
|
18
|
+
If workflows exist, the state is a transient race condition and the loop keeps waiting.
|
|
19
|
+
|
|
20
|
+
**Bug B — No process exit after session ends:** After a successful run (PR became mergeable,
|
|
21
|
+
work session ended), `solve.mjs` never called `process.exit()`. Sentry's profiling integration
|
|
22
|
+
(`@sentry/profiling-node`) kept the Node.js event loop alive indefinitely. Fixed by calling
|
|
23
|
+
`safeExit(0)` at the end of the `finally` block in `solve.mjs`, which flushes Sentry events
|
|
24
|
+
(up to 2 seconds) and then calls `process.exit(0)`.
|
|
25
|
+
|
|
26
|
+
Also adds `--verbose` debug logging of active Node.js handles at exit to aid diagnosis of
|
|
27
|
+
future occurrences.
|
|
28
|
+
|
|
29
|
+
## 1.31.0
|
|
30
|
+
|
|
31
|
+
### Minor Changes
|
|
32
|
+
|
|
33
|
+
- feat: add --finalize option (Issue #1383)
|
|
34
|
+
|
|
35
|
+
Adds new experimental CLI options to the `solve` command:
|
|
36
|
+
- `--finalize [N]`: After the main solve completes, automatically restarts the AI tool N times (default: 1 when used as a flag) with a requirements-check prompt to verify all requirements are met. Uses the same model as `--model` by default.
|
|
37
|
+
- `--finalize-model`: Override the model used during `--finalize` iterations (defaults to `--model`).
|
|
38
|
+
- `--prompt-ensure-all-requirements-are-met`: Adds a system prompt hint in the "Self review" section instructing the AI to ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements. Enabled automatically during `--finalize` iterations only (not the first regular run).
|
|
39
|
+
|
|
40
|
+
This forces the AI tool to double-check itself after the main solve, verifying changes meet all requirements from the issue description and PR comments, and that CI/CD checks pass.
|
|
41
|
+
|
|
42
|
+
feat: auto-commit uncommitted changes and upload log on CTRL+C interrupt (Issue #1351)
|
|
43
|
+
|
|
44
|
+
Previously, when a user pressed CTRL+C to interrupt a running solve session, uncommitted changes were silently lost (or left uncommitted) and log files were not uploaded to the PR/issue even when `--attach-logs` was enabled. Additionally, the terminal showed "Claude command completed" instead of "Claude command interrupted".
|
|
45
|
+
|
|
46
|
+
Now on CTRL+C:
|
|
47
|
+
1. **Auto-commit**: Any uncommitted changes in the working directory are automatically committed and pushed to the branch before cleanup occurs.
|
|
48
|
+
2. **Log upload**: If `--attach-logs` is enabled, the log file is automatically uploaded to the GitHub PR/issue as a comment.
|
|
49
|
+
3. **Accurate message**: The terminal now correctly shows "Claude command interrupted" instead of "Claude command completed" when the process exits with code 130 (SIGINT).
|
|
50
|
+
|
|
51
|
+
Changes made:
|
|
52
|
+
- `src/exit-handler.lib.mjs`: Added optional `interrupt` parameter to `initializeExitHandler()`; SIGINT handler now calls it before cleanup, guarded against double invocation
|
|
53
|
+
- `src/solve.mjs`: Extended `cleanupContext` with branch/PR/owner/repo fields; new `interruptWrapper` auto-commits and uploads logs on CTRL+C
|
|
54
|
+
- `src/claude.lib.mjs`, `src/opencode.lib.mjs`, `src/codex.lib.mjs`, `src/agent.lib.mjs`: Detect exit code 130 and print "interrupted" instead of "completed"
|
|
55
|
+
|
|
56
|
+
Full case study analysis including timeline reconstruction, root cause analysis, and implementation details in `docs/case-studies/issue-1351/`.
|
|
57
|
+
|
|
58
|
+
fix: prevent false positive ready tag sync by using issue timeline API (Issue #1413)
|
|
59
|
+
|
|
60
|
+
Previously, `syncReadyTags()` used a GitHub full-text body search to find PRs linked to an issue:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
gh pr list --search "in:body closes #1411 OR fixes #1411 OR resolves #1411"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This caused a false positive: PR #843 matched because `1411` appeared as a source code line reference inside its body, not as a genuine issue-closing keyword.
|
|
67
|
+
|
|
68
|
+
Now uses the GitHub issue timeline API (`GET /repos/{owner}/{repo}/issues/{issue_number}/timeline`) to find PRs with genuine `cross-referenced` events, which is the same data GitHub uses to auto-close issues when PRs are merged.
|
|
69
|
+
|
|
70
|
+
fix: hide cancel button and show cancelling state on /merge cancel (Issue #1407)
|
|
71
|
+
|
|
72
|
+
When user clicked the "🛑 Cancel" button during `/merge` queue processing, the cancel button remained visible in the Telegram message until the current PR finished processing (potentially hours if waiting for CI). The toast message "The current PR will finish processing" was also confusing.
|
|
73
|
+
|
|
74
|
+
The fix immediately hides the cancel button by editing the message without `reply_markup`, shows a "🛑 Cancelling..." indicator in the progress message when cancellation is requested, and adds `isCancelled` support to `waitForCI()` for early exit when the operation is cancelled.
|
|
75
|
+
|
|
3
76
|
## 1.30.5
|
|
4
77
|
|
|
5
78
|
### Patch Changes
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -911,6 +911,9 @@ export const executeAgentCommand = async params => {
|
|
|
911
911
|
// Explicit JSON error message from agent (Issue #1201: includes streaming-detected errors)
|
|
912
912
|
errorInfo.message = `Agent reported error: ${outputError.match}`;
|
|
913
913
|
await log(`\n\n❌ ${errorInfo.message}`, { level: 'error' });
|
|
914
|
+
} else if (exitCode === 130) {
|
|
915
|
+
errorInfo.message = 'Agent command interrupted (CTRL+C)';
|
|
916
|
+
await log('\n\n⚠️ Agent command interrupted (CTRL+C)');
|
|
914
917
|
} else {
|
|
915
918
|
errorInfo.message = `Agent command failed with exit code ${exitCode}`;
|
|
916
919
|
await log(`\n\n❌ ${errorInfo.message}`, { level: 'error' });
|
|
@@ -212,7 +212,12 @@ Self review.
|
|
|
212
212
|
- When you check your solution draft, run all tests locally.
|
|
213
213
|
- When you check your solution draft, verify git status shows a clean working tree with no uncommitted changes.
|
|
214
214
|
- When you compare with repo style, use gh pr diff [number].
|
|
215
|
-
- When you finalize, confirm code, tests, and description are consistent
|
|
215
|
+
- When you finalize, confirm code, tests, and description are consistent.${
|
|
216
|
+
argv && argv.promptEnsureAllRequirementsAreMet
|
|
217
|
+
? `
|
|
218
|
+
- When no explicit feedback or requirements is provided, ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements (check issue description and all comments in issue and in pull request). Ensure all CI/CD checks pass.`
|
|
219
|
+
: ''
|
|
220
|
+
}
|
|
216
221
|
|
|
217
222
|
GitHub CLI command patterns.
|
|
218
223
|
- IMPORTANT: Always use --paginate flag when fetching lists from GitHub API to ensure all results are returned (GitHub returns max 30 per page by default).
|
package/src/claude.lib.mjs
CHANGED
|
@@ -1295,7 +1295,10 @@ export const executeClaudeCommand = async params => {
|
|
|
1295
1295
|
}
|
|
1296
1296
|
// Issue #1088: If error_during_execution occurred but command didn't fail,
|
|
1297
1297
|
// log it as "Finished with errors" instead of pure success
|
|
1298
|
-
|
|
1298
|
+
// Issue #1351: Distinguish interrupted sessions (exit code 130) from normal completion
|
|
1299
|
+
if (exitCode === 130) {
|
|
1300
|
+
await log('\n\n⚠️ Claude command interrupted (CTRL+C)');
|
|
1301
|
+
} else if (errorDuringExecution) {
|
|
1299
1302
|
await log('\n\n⚠️ Claude command finished with errors');
|
|
1300
1303
|
} else {
|
|
1301
1304
|
await log('\n\n✅ Claude command completed');
|
|
@@ -254,7 +254,12 @@ Self review.
|
|
|
254
254
|
- When you check your solution draft, run all tests locally.
|
|
255
255
|
- When you check your solution draft, verify git status shows a clean working tree with no uncommitted changes.
|
|
256
256
|
- When you compare with repo style, use gh pr diff [number].
|
|
257
|
-
- When you finalize, confirm code, tests, and description are consistent
|
|
257
|
+
- When you finalize, confirm code, tests, and description are consistent.${
|
|
258
|
+
argv && argv.promptEnsureAllRequirementsAreMet
|
|
259
|
+
? `
|
|
260
|
+
- When no explicit feedback or requirements is provided, ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements (check issue description and all comments in issue and in pull request). Ensure all CI/CD checks pass.`
|
|
261
|
+
: ''
|
|
262
|
+
}
|
|
258
263
|
|
|
259
264
|
GitHub CLI command patterns.
|
|
260
265
|
- IMPORTANT: Always use --paginate flag when fetching lists from GitHub API to ensure all results are returned (GitHub returns max 30 per page by default).
|
package/src/codex.lib.mjs
CHANGED
|
@@ -398,6 +398,8 @@ export const executeCodexCommand = async params => {
|
|
|
398
398
|
for (const line of messageLines) {
|
|
399
399
|
await log(line, { level: 'warning' });
|
|
400
400
|
}
|
|
401
|
+
} else if (exitCode === 130) {
|
|
402
|
+
await log('\n\n⚠️ Codex command interrupted (CTRL+C)');
|
|
401
403
|
} else {
|
|
402
404
|
await log(`\n\n❌ Codex command failed with exit code ${exitCode}`, { level: 'error' });
|
|
403
405
|
}
|
|
@@ -220,7 +220,12 @@ Self review.
|
|
|
220
220
|
- When you check your solution draft, run all tests locally.
|
|
221
221
|
- When you check your solution draft, verify git status shows a clean working tree with no uncommitted changes.
|
|
222
222
|
- When you compare with repo style, use gh pr diff [number].
|
|
223
|
-
- When you finalize, confirm code, tests, and description are consistent
|
|
223
|
+
- When you finalize, confirm code, tests, and description are consistent.${
|
|
224
|
+
argv && argv.promptEnsureAllRequirementsAreMet
|
|
225
|
+
? `
|
|
226
|
+
- When no explicit feedback or requirements is provided, ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements (check issue description and all comments in issue and in pull request). Ensure all CI/CD checks pass.`
|
|
227
|
+
: ''
|
|
228
|
+
}
|
|
224
229
|
|
|
225
230
|
GitHub CLI command patterns.
|
|
226
231
|
- IMPORTANT: Always use --paginate flag when fetching lists from GitHub API to ensure all results are returned (GitHub returns max 30 per page by default).
|
package/src/exit-handler.lib.mjs
CHANGED
|
@@ -25,17 +25,22 @@ let exitMessageShown = false;
|
|
|
25
25
|
let getLogPathFunction = null;
|
|
26
26
|
let logFunction = null;
|
|
27
27
|
let cleanupFunction = null;
|
|
28
|
+
let interruptFunction = null;
|
|
29
|
+
let interruptHandlerRan = false;
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Initialize the exit handler with required dependencies
|
|
31
33
|
* @param {Function} getLogPath - Function that returns the current log path
|
|
32
34
|
* @param {Function} log - Logging function
|
|
33
35
|
* @param {Function} cleanup - Optional cleanup function to call on exit
|
|
36
|
+
* @param {Function} interrupt - Optional interrupt function to call on SIGINT/SIGTERM before cleanup
|
|
37
|
+
* (e.g., auto-commit uncommitted changes, upload logs)
|
|
34
38
|
*/
|
|
35
|
-
export const initializeExitHandler = (getLogPath, log, cleanup = null) => {
|
|
39
|
+
export const initializeExitHandler = (getLogPath, log, cleanup = null, interrupt = null) => {
|
|
36
40
|
getLogPathFunction = getLogPath;
|
|
37
41
|
logFunction = log;
|
|
38
42
|
cleanupFunction = cleanup;
|
|
43
|
+
interruptFunction = interrupt;
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -114,6 +119,15 @@ export const installGlobalExitHandlers = () => {
|
|
|
114
119
|
|
|
115
120
|
// Handle SIGINT (CTRL+C)
|
|
116
121
|
process.on('SIGINT', async () => {
|
|
122
|
+
// Run interrupt handler first (auto-commit, log upload, etc.) — guard against double invocation
|
|
123
|
+
if (interruptFunction && !interruptHandlerRan) {
|
|
124
|
+
interruptHandlerRan = true;
|
|
125
|
+
try {
|
|
126
|
+
await interruptFunction();
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore interrupt handler errors
|
|
129
|
+
}
|
|
130
|
+
}
|
|
117
131
|
if (cleanupFunction) {
|
|
118
132
|
try {
|
|
119
133
|
await cleanupFunction();
|
|
@@ -208,4 +222,5 @@ export const installGlobalExitHandlers = () => {
|
|
|
208
222
|
*/
|
|
209
223
|
export const resetExitHandler = () => {
|
|
210
224
|
exitMessageShown = false;
|
|
225
|
+
interruptHandlerRan = false;
|
|
211
226
|
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Merge Ready Tag Sync Library
|
|
4
|
+
*
|
|
5
|
+
* Provides utilities for syncing 'ready' tags between linked PRs and issues,
|
|
6
|
+
* and for finding genuinely linked PRs via the GitHub issue timeline API.
|
|
7
|
+
* Split from github-merge.lib.mjs to maintain file size limits.
|
|
8
|
+
*
|
|
9
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1413
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
import { exec as execCallback } from 'child_process';
|
|
14
|
+
|
|
15
|
+
const exec = promisify(execCallback);
|
|
16
|
+
|
|
17
|
+
import { extractLinkedIssueNumber } from './github-linking.lib.mjs';
|
|
18
|
+
|
|
19
|
+
// READY_LABEL is also exported from github-merge.lib.mjs (which re-exports it from here)
|
|
20
|
+
export const READY_LABEL = {
|
|
21
|
+
name: 'ready',
|
|
22
|
+
description: 'Is ready to be merged',
|
|
23
|
+
color: '0E8A16', // Green color
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Add a label to a GitHub issue or pull request
|
|
28
|
+
* @param {'issue'|'pr'} type - Whether to add to issue or PR
|
|
29
|
+
* @param {string} owner - Repository owner
|
|
30
|
+
* @param {string} repo - Repository name
|
|
31
|
+
* @param {number} number - Issue or PR number
|
|
32
|
+
* @param {string} labelName - Label name to add
|
|
33
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
34
|
+
* @returns {Promise<{success: boolean, error: string|null}>}
|
|
35
|
+
*/
|
|
36
|
+
async function addLabel(type, owner, repo, number, labelName, verbose = false) {
|
|
37
|
+
const cmd = type === 'issue' ? 'issue' : 'pr';
|
|
38
|
+
try {
|
|
39
|
+
await exec(`gh ${cmd} edit ${number} --repo ${owner}/${repo} --add-label "${labelName}"`);
|
|
40
|
+
if (verbose) console.log(`[VERBOSE] /merge: Added '${labelName}' label to ${type} #${number}`);
|
|
41
|
+
return { success: true, error: null };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (verbose) console.log(`[VERBOSE] /merge: Failed to add label to ${type} #${number}: ${error.message}`);
|
|
44
|
+
return { success: false, error: error.message };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get open PRs that are genuinely linked to an issue via GitHub's issue timeline.
|
|
50
|
+
*
|
|
51
|
+
* Issue #1413: This replaces the previous full-text body search approach which
|
|
52
|
+
* caused false positives. For example, a search for `fixes #1411` would incorrectly
|
|
53
|
+
* match PR #843 because its body contained the string `1411→` as a source code line
|
|
54
|
+
* number in a code snippet — not as an issue closing reference.
|
|
55
|
+
*
|
|
56
|
+
* The GitHub issue timeline API returns `cross-referenced` events for PRs that
|
|
57
|
+
* explicitly close the issue using GitHub's reserved keywords (fixes/closes/resolves).
|
|
58
|
+
* This is the same data GitHub uses to auto-close issues when PRs are merged, so
|
|
59
|
+
* it reliably identifies genuine closing references.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} owner - Repository owner
|
|
62
|
+
* @param {string} repo - Repository name
|
|
63
|
+
* @param {number} issueNumber - Issue number to find linked PRs for
|
|
64
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
65
|
+
* @returns {Promise<Array<{number: number, title: string}>>} Array of open PRs that close this issue
|
|
66
|
+
*/
|
|
67
|
+
export async function getLinkedPRsFromTimeline(owner, repo, issueNumber, verbose = false) {
|
|
68
|
+
try {
|
|
69
|
+
const { stdout: timelineJson } = await exec(`gh api repos/${owner}/${repo}/issues/${issueNumber}/timeline --paginate`);
|
|
70
|
+
const timeline = JSON.parse(timelineJson.trim() || '[]');
|
|
71
|
+
|
|
72
|
+
// Extract cross-referenced events where the source is an open PR
|
|
73
|
+
// (source.issue.pull_request != null means the source is a PR, not a plain issue)
|
|
74
|
+
const linkedPRNumbers = new Set();
|
|
75
|
+
const linkedPRs = [];
|
|
76
|
+
|
|
77
|
+
for (const event of timeline) {
|
|
78
|
+
if (event.event === 'cross-referenced' && event.source?.issue?.pull_request != null && event.source?.issue?.state === 'open') {
|
|
79
|
+
const prNumber = event.source.issue.number;
|
|
80
|
+
if (!linkedPRNumbers.has(prNumber)) {
|
|
81
|
+
linkedPRNumbers.add(prNumber);
|
|
82
|
+
linkedPRs.push({
|
|
83
|
+
number: prNumber,
|
|
84
|
+
title: event.source.issue.title || '',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (verbose) {
|
|
91
|
+
console.log(`[VERBOSE] /merge: Issue #${issueNumber} has ${linkedPRs.length} genuinely linked open PR(s) via timeline`);
|
|
92
|
+
for (const pr of linkedPRs) {
|
|
93
|
+
console.log(`[VERBOSE] /merge: PR #${pr.number}: ${pr.title}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return linkedPRs;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (verbose) {
|
|
100
|
+
console.log(`[VERBOSE] /merge: Error fetching timeline for issue #${issueNumber}: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sync 'ready' tags between linked pull requests and issues
|
|
108
|
+
*
|
|
109
|
+
* Issue #1367: Before building the merge queue, ensure that:
|
|
110
|
+
* 1. If a PR has 'ready' label and is clearly linked to an issue (via standard GitHub
|
|
111
|
+
* keywords in the PR body/title), the issue also gets 'ready' label.
|
|
112
|
+
* 2. If an issue has 'ready' label and has a clearly linked open PR, the PR also gets
|
|
113
|
+
* 'ready' label.
|
|
114
|
+
*
|
|
115
|
+
* This ensures the final list of ready PRs reflects all ready work, regardless of
|
|
116
|
+
* where the 'ready' label was originally applied.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} owner - Repository owner
|
|
119
|
+
* @param {string} repo - Repository name
|
|
120
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
121
|
+
* @returns {Promise<{synced: number, errors: number, details: Array<Object>}>}
|
|
122
|
+
*/
|
|
123
|
+
export async function syncReadyTags(owner, repo, verbose = false) {
|
|
124
|
+
const synced = [];
|
|
125
|
+
const errors = [];
|
|
126
|
+
|
|
127
|
+
if (verbose) {
|
|
128
|
+
console.log(`[VERBOSE] /merge: Syncing 'ready' tags for ${owner}/${repo}...`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Fetch open PRs with 'ready' label (including body for link detection)
|
|
133
|
+
const { stdout: prsJson } = await exec(`gh pr list --repo ${owner}/${repo} --label "${READY_LABEL.name}" --state open --json number,title,body,labels --limit 100`);
|
|
134
|
+
const readyPRs = JSON.parse(prsJson.trim() || '[]');
|
|
135
|
+
|
|
136
|
+
if (verbose) {
|
|
137
|
+
console.log(`[VERBOSE] /merge: Found ${readyPRs.length} open PRs with 'ready' label for tag sync`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fetch open issues with 'ready' label
|
|
141
|
+
const { stdout: issuesJson } = await exec(`gh issue list --repo ${owner}/${repo} --label "${READY_LABEL.name}" --state open --json number,title --limit 100`);
|
|
142
|
+
const readyIssues = JSON.parse(issuesJson.trim() || '[]');
|
|
143
|
+
|
|
144
|
+
if (verbose) {
|
|
145
|
+
console.log(`[VERBOSE] /merge: Found ${readyIssues.length} open issues with 'ready' label for tag sync`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Build a set of issue numbers that already have 'ready'
|
|
149
|
+
const readyIssueNumbers = new Set(readyIssues.map(i => String(i.number)));
|
|
150
|
+
|
|
151
|
+
// Step 1: For each PR with 'ready', find linked issue and sync label to it
|
|
152
|
+
for (const pr of readyPRs) {
|
|
153
|
+
try {
|
|
154
|
+
const prBody = pr.body || '';
|
|
155
|
+
const linkedIssueNumber = extractLinkedIssueNumber(prBody);
|
|
156
|
+
|
|
157
|
+
if (!linkedIssueNumber) {
|
|
158
|
+
if (verbose) {
|
|
159
|
+
console.log(`[VERBOSE] /merge: PR #${pr.number} has no linked issue (no closing keyword in body)`);
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (readyIssueNumbers.has(String(linkedIssueNumber))) {
|
|
165
|
+
if (verbose) {
|
|
166
|
+
console.log(`[VERBOSE] /merge: Issue #${linkedIssueNumber} already has 'ready' label (linked from PR #${pr.number})`);
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Issue doesn't have 'ready' label yet - add it
|
|
172
|
+
if (verbose) {
|
|
173
|
+
console.log(`[VERBOSE] /merge: PR #${pr.number} has 'ready', adding to linked issue #${linkedIssueNumber}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await addLabel('issue', owner, repo, linkedIssueNumber, READY_LABEL.name, verbose);
|
|
177
|
+
if (result.success) {
|
|
178
|
+
synced.push({ type: 'pr-to-issue', prNumber: pr.number, issueNumber: Number(linkedIssueNumber) });
|
|
179
|
+
// Mark this issue as now having 'ready' so we don't process it again
|
|
180
|
+
readyIssueNumbers.add(String(linkedIssueNumber));
|
|
181
|
+
} else {
|
|
182
|
+
errors.push({ type: 'pr-to-issue', prNumber: pr.number, issueNumber: Number(linkedIssueNumber), error: result.error });
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (verbose) {
|
|
186
|
+
console.log(`[VERBOSE] /merge: Error syncing label from PR #${pr.number}: ${err.message}`);
|
|
187
|
+
}
|
|
188
|
+
errors.push({ type: 'pr-to-issue', prNumber: pr.number, error: err.message });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build a set of PR numbers that already have 'ready'
|
|
193
|
+
const readyPRNumbers = new Set(readyPRs.map(p => String(p.number)));
|
|
194
|
+
|
|
195
|
+
// Step 2: For each issue with 'ready', find linked PRs and sync label to them
|
|
196
|
+
for (const issue of readyIssues) {
|
|
197
|
+
try {
|
|
198
|
+
// Issue #1413: Use the GitHub issue timeline API to find PRs that genuinely
|
|
199
|
+
// close this issue via closing keywords. This avoids false positives from
|
|
200
|
+
// full-text search, which can match PRs that contain the issue number as a
|
|
201
|
+
// source code line number (e.g. "1411→ await log(...)") rather than as a
|
|
202
|
+
// real closing reference.
|
|
203
|
+
const linkedPRs = await getLinkedPRsFromTimeline(owner, repo, issue.number, verbose);
|
|
204
|
+
|
|
205
|
+
for (const linkedPR of linkedPRs) {
|
|
206
|
+
if (readyPRNumbers.has(String(linkedPR.number))) {
|
|
207
|
+
if (verbose) {
|
|
208
|
+
console.log(`[VERBOSE] /merge: PR #${linkedPR.number} already has 'ready' label (linked from issue #${issue.number})`);
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// PR doesn't have 'ready' label yet - add it
|
|
214
|
+
if (verbose) {
|
|
215
|
+
console.log(`[VERBOSE] /merge: Issue #${issue.number} has 'ready', adding to linked PR #${linkedPR.number}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const result = await addLabel('pr', owner, repo, linkedPR.number, READY_LABEL.name, verbose);
|
|
219
|
+
if (result.success) {
|
|
220
|
+
synced.push({ type: 'issue-to-pr', issueNumber: issue.number, prNumber: linkedPR.number });
|
|
221
|
+
// Mark this PR as now having 'ready'
|
|
222
|
+
readyPRNumbers.add(String(linkedPR.number));
|
|
223
|
+
} else {
|
|
224
|
+
errors.push({ type: 'issue-to-pr', issueNumber: issue.number, prNumber: linkedPR.number, error: result.error });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (verbose) {
|
|
229
|
+
console.log(`[VERBOSE] /merge: Error syncing label from issue #${issue.number}: ${err.message}`);
|
|
230
|
+
}
|
|
231
|
+
errors.push({ type: 'issue-to-pr', issueNumber: issue.number, error: err.message });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (verbose) {
|
|
236
|
+
console.log(`[VERBOSE] /merge: Error during tag sync: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
errors.push({ type: 'fetch', error: error.message });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (verbose) {
|
|
242
|
+
console.log(`[VERBOSE] /merge: Tag sync complete. Synced: ${synced.length}, Errors: ${errors.length}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
synced: synced.length,
|
|
247
|
+
errors: errors.length,
|
|
248
|
+
details: synced,
|
|
249
|
+
errorDetails: errors,
|
|
250
|
+
};
|
|
251
|
+
}
|