@link-assistant/hive-mind 1.69.0 → 1.69.2
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 +12 -0
- package/package.json +1 -1
- package/src/github-linking.lib.mjs +50 -45
- package/src/github.batch.lib.mjs +36 -62
- package/src/solve.feedback.lib.mjs +7 -3
- package/src/solve.mjs +1 -0
- package/src/solve.preparation.lib.mjs +2 -1
- package/src/solve.watch.lib.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.69.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0ff36b4: Count open draft pull requests when `/hive --skip-issues-with-prs` checks linked solution drafts, preventing duplicate work while an existing PR is still in progress.
|
|
8
|
+
|
|
9
|
+
## 1.69.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2911597: Fix feedback comment counting to run local git timestamp checks in the prepared repository directory, avoiding misleading `not a git repository` diagnostics in detached solve sessions.
|
|
14
|
+
|
|
3
15
|
## 1.69.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -26,66 +26,71 @@ export function getGitHubLinkingKeywords() {
|
|
|
26
26
|
return ['close', 'closes', 'closed', 'fix', 'fixes', 'fixed', 'resolve', 'resolves', 'resolved'];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* @param {string} prBody - The pull request body text
|
|
33
|
-
* @param {string|number} issueNumber - The issue number to check for
|
|
34
|
-
* @param {string} [owner] - Repository owner (for cross-repo references)
|
|
35
|
-
* @param {string} [repo] - Repository name (for cross-repo references)
|
|
36
|
-
* @returns {boolean} True if a valid linking keyword is found
|
|
37
|
-
*/
|
|
38
|
-
export function hasGitHubLinkingKeyword(prBody, issueNumber, owner = null, repo = null) {
|
|
39
|
-
if (!prBody || !issueNumber) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
29
|
+
function escapeRegExp(value) {
|
|
30
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
31
|
+
}
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
const issueNumStr = issueNumber
|
|
33
|
+
function buildClosingReferencePatterns(keyword, issueNumber, owner = null, repo = null) {
|
|
34
|
+
const issueNumStr = escapeRegExp(issueNumber);
|
|
35
|
+
const separator = String.raw`(?:\s+|\s*:\s*)`;
|
|
36
|
+
const prefix = String.raw`\b${keyword}${separator}`;
|
|
37
|
+
const references = [String.raw`#${issueNumStr}\b`];
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
if (owner && repo) {
|
|
40
|
+
references.push(`${escapeRegExp(owner)}/${escapeRegExp(repo)}#${issueNumStr}\\b`);
|
|
41
|
+
references.push(`https://github\\.com/${escapeRegExp(owner)}/${escapeRegExp(repo)}/issues/${issueNumStr}\\b`);
|
|
42
|
+
}
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Must have word boundary before keyword and # immediately before number
|
|
54
|
-
const pattern1 = new RegExp(`\\b${keyword}\\s+#${issueNumStr}\\b`, 'i');
|
|
44
|
+
references.push(String.raw`[\w.-]+/[\w.-]+#${issueNumStr}\b`);
|
|
45
|
+
references.push(`https://github\\.com/[^/\\s]+/[^/\\s]+/issues/${issueNumStr}\\b`);
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Pattern 2: KEYWORD owner/repo#123 (for cross-repo or fork references)
|
|
61
|
-
if (owner && repo) {
|
|
62
|
-
const pattern2 = new RegExp(`\\b${keyword}\\s+${owner}/${repo}#${issueNumStr}\\b`, 'i');
|
|
47
|
+
return references.map(reference => new RegExp(`${prefix}${reference}`, 'i'));
|
|
48
|
+
}
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Check whether text contains a GitHub closing keyword for a specific issue.
|
|
52
|
+
*
|
|
53
|
+
* This is the shared parser used by solve and hive code paths so they agree on
|
|
54
|
+
* which PR body/title references are real closing links.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} text - Pull request body or title text
|
|
57
|
+
* @param {string|number} issueNumber - Issue number to check for
|
|
58
|
+
* @param {string} [owner] - Repository owner for exact owner/repo references
|
|
59
|
+
* @param {string} [repo] - Repository name for exact owner/repo references
|
|
60
|
+
* @returns {boolean} True if a valid closing reference is found
|
|
61
|
+
*/
|
|
62
|
+
export function prClosesIssue(text, issueNumber, owner = null, repo = null) {
|
|
63
|
+
if (!text || typeof text !== 'string' || issueNumber === null || issueNumber === undefined || String(issueNumber).trim() === '') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
if (owner && repo) {
|
|
71
|
-
const pattern3 = new RegExp(`\\b${keyword}\\s+https://github\\.com/${owner}/${repo}/issues/${issueNumStr}\\b`, 'i');
|
|
67
|
+
const issueNumStr = String(issueNumber).trim();
|
|
72
68
|
|
|
73
|
-
|
|
69
|
+
for (const keyword of getGitHubLinkingKeywords()) {
|
|
70
|
+
const patterns = buildClosingReferencePatterns(keyword, issueNumStr, owner, repo);
|
|
71
|
+
for (const pattern of patterns) {
|
|
72
|
+
if (pattern.test(text)) {
|
|
74
73
|
return true;
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
|
-
|
|
78
|
-
// Pattern 4: Also check for any URL format (generic)
|
|
79
|
-
const pattern4 = new RegExp(`\\b${keyword}\\s+https://github\\.com/[^/]+/[^/]+/issues/${issueNumStr}\\b`, 'i');
|
|
80
|
-
|
|
81
|
-
if (pattern4.test(prBody)) {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
return false;
|
|
87
79
|
}
|
|
88
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Check if PR body contains a valid GitHub linking keyword for the given issue
|
|
83
|
+
*
|
|
84
|
+
* @param {string} prBody - The pull request body text
|
|
85
|
+
* @param {string|number} issueNumber - The issue number to check for
|
|
86
|
+
* @param {string} [owner] - Repository owner (for cross-repo references)
|
|
87
|
+
* @param {string} [repo] - Repository name (for cross-repo references)
|
|
88
|
+
* @returns {boolean} True if a valid linking keyword is found
|
|
89
|
+
*/
|
|
90
|
+
export function hasGitHubLinkingKeyword(prBody, issueNumber, owner = null, repo = null) {
|
|
91
|
+
return prClosesIssue(prBody, issueNumber, owner, repo);
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
/**
|
|
90
95
|
* Extract issue number from PR body using GitHub linking keywords
|
|
91
96
|
* This is used to find which issue a PR is linked to
|
package/src/github.batch.lib.mjs
CHANGED
|
@@ -10,53 +10,46 @@ if (typeof globalThis.use === 'undefined') {
|
|
|
10
10
|
// Import dependencies
|
|
11
11
|
import { log, cleanErrorMessage } from './lib.mjs';
|
|
12
12
|
import { githubLimits, timeouts } from './config.lib.mjs';
|
|
13
|
+
import { prClosesIssue } from './github-linking.lib.mjs';
|
|
13
14
|
|
|
14
15
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. execGhWithRetry adds transient-network retry (#1756).
|
|
16
|
+
export { prClosesIssue };
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {number}
|
|
21
|
-
* @
|
|
19
|
+
* Extract open pull requests that are linked to an issue with closing keywords.
|
|
20
|
+
* Draft pull requests are still open in-progress solution drafts, so they must
|
|
21
|
+
* count for /hive --skip-issues-with-prs.
|
|
22
|
+
* @param {Object} issueData - GraphQL issue node with timelineItems
|
|
23
|
+
* @param {number} issueNum - Issue number to check
|
|
24
|
+
* @param {Function} logger - Async logger, defaults to shared log helper
|
|
25
|
+
* @returns {Promise<Array<Object>>} Linked open PRs that close the issue
|
|
22
26
|
*/
|
|
23
|
-
export function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Must be followed by word boundary (not part of larger number)
|
|
45
|
-
const patterns = [
|
|
46
|
-
// Standard format: "fixes #123"
|
|
47
|
-
new RegExp(`\\b${keyword}\\s*:?\\s*#${issueNum}\\b`, 'i'),
|
|
48
|
-
// With repo prefix: "fixes owner/repo#123"
|
|
49
|
-
new RegExp(`\\b${keyword}\\s*:?\\s*[\\w.-]+/[\\w.-]+#${issueNum}\\b`, 'i'),
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
for (const pattern of patterns) {
|
|
53
|
-
if (pattern.test(text)) {
|
|
54
|
-
return true;
|
|
27
|
+
export async function extractLinkedPullRequestsForIssue(issueData, issueNum, logger = log) {
|
|
28
|
+
const linkedPRs = [];
|
|
29
|
+
|
|
30
|
+
for (const item of issueData.timelineItems?.nodes || []) {
|
|
31
|
+
if (item?.source && item.source.state === 'OPEN') {
|
|
32
|
+
// Check if PR actually closes this issue (has "fixes #N", "closes #N", or "resolves #N")
|
|
33
|
+
const prBody = item.source.body || '';
|
|
34
|
+
const prTitle = item.source.title || '';
|
|
35
|
+
const closesThisIssue = prClosesIssue(prBody, issueNum) || prClosesIssue(prTitle, issueNum);
|
|
36
|
+
|
|
37
|
+
if (closesThisIssue) {
|
|
38
|
+
linkedPRs.push({
|
|
39
|
+
number: item.source.number,
|
|
40
|
+
title: item.source.title,
|
|
41
|
+
state: item.source.state,
|
|
42
|
+
isDraft: Boolean(item.source.isDraft),
|
|
43
|
+
url: item.source.url,
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
// Log that we're skipping a PR that only mentions the issue
|
|
47
|
+
await logger(` ℹ️ PR #${item.source.number} mentions issue #${issueNum} but doesn't close it (no fixes/closes/resolves keyword)`, { verbose: true });
|
|
55
48
|
}
|
|
56
49
|
}
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
return
|
|
52
|
+
return linkedPRs;
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
/**
|
|
@@ -140,31 +133,11 @@ export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers)
|
|
|
140
133
|
for (const issueNum of batch) {
|
|
141
134
|
const issueData = data.data?.repository?.[`issue${issueNum}`];
|
|
142
135
|
if (issueData) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Extract linked PRs from timeline items
|
|
136
|
+
// Extract linked PRs from timeline items, including draft PRs.
|
|
146
137
|
// Issue #1094: Only count PRs that explicitly fix/close/resolve this issue
|
|
147
138
|
// This prevents false positives from PRs that only mention issues without solving them
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Check if PR actually closes this issue (has "fixes #N", "closes #N", or "resolves #N")
|
|
151
|
-
const prBody = item.source.body || '';
|
|
152
|
-
const prTitle = item.source.title || '';
|
|
153
|
-
const closesThisIssue = prClosesIssue(prBody, issueNum) || prClosesIssue(prTitle, issueNum);
|
|
154
|
-
|
|
155
|
-
if (closesThisIssue) {
|
|
156
|
-
linkedPRs.push({
|
|
157
|
-
number: item.source.number,
|
|
158
|
-
title: item.source.title,
|
|
159
|
-
state: item.source.state,
|
|
160
|
-
url: item.source.url,
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
// Log that we're skipping a PR that only mentions the issue
|
|
164
|
-
await log(` ℹ️ PR #${item.source.number} mentions issue #${issueNum} but doesn't close it (no fixes/closes/resolves keyword)`, { verbose: true });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
139
|
+
// Issue #1760: Draft PRs are still active solution drafts and must block duplicate work
|
|
140
|
+
const linkedPRs = await extractLinkedPullRequestsForIssue(issueData, issueNum);
|
|
168
141
|
|
|
169
142
|
results[issueNum] = {
|
|
170
143
|
title: issueData.title,
|
|
@@ -337,6 +310,7 @@ export async function batchCheckArchivedRepositories(repositories) {
|
|
|
337
310
|
// Export all functions as default object too
|
|
338
311
|
export default {
|
|
339
312
|
prClosesIssue,
|
|
313
|
+
extractLinkedPullRequestsForIssue,
|
|
340
314
|
batchCheckPullRequestsForIssues,
|
|
341
315
|
batchCheckArchivedRepositories,
|
|
342
316
|
};
|
|
@@ -8,7 +8,7 @@ import { reportError } from './sentry.lib.mjs';
|
|
|
8
8
|
|
|
9
9
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
|
|
10
10
|
export const detectAndCountFeedback = async params => {
|
|
11
|
-
const { prNumber, branchName, owner, repo, issueNumber, isContinueMode, argv, mergeStateStatus, prState, workStartTime, log, formatAligned, cleanErrorMessage,
|
|
11
|
+
const { prNumber, branchName, owner, repo, issueNumber, isContinueMode, argv, mergeStateStatus, prState, workStartTime, log, formatAligned, cleanErrorMessage, $, repositoryPath = null } = params;
|
|
12
12
|
|
|
13
13
|
let newPrComments = 0;
|
|
14
14
|
let newPrReviewComments = 0;
|
|
@@ -53,14 +53,18 @@ export const detectAndCountFeedback = async params => {
|
|
|
53
53
|
if (argv.verbose) {
|
|
54
54
|
await log(` PR #${prNumber} on branch: ${branchName}`, { verbose: true });
|
|
55
55
|
await log(` Owner/Repo: ${owner}/${repo}`, { verbose: true });
|
|
56
|
+
if (repositoryPath) {
|
|
57
|
+
await log(` Repository path: ${repositoryPath}`, { verbose: true });
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
// Get the last commit timestamp from the PR branch
|
|
59
62
|
let lastCommitTime = null;
|
|
60
|
-
|
|
63
|
+
const git$ = repositoryPath ? $({ cwd: repositoryPath }) : $;
|
|
64
|
+
let lastCommitResult = await git$`git log -1 --format="%aI" origin/${branchName}`;
|
|
61
65
|
if (lastCommitResult.code !== 0) {
|
|
62
66
|
// Fallback to local branch if remote doesn't exist
|
|
63
|
-
lastCommitResult = await $`git log -1 --format="%aI" ${branchName}`;
|
|
67
|
+
lastCommitResult = await git$`git log -1 --format="%aI" ${branchName}`;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
if (lastCommitResult.code === 0) {
|
package/src/solve.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-l
|
|
|
8
8
|
const feedback = await import('./solve.feedback.lib.mjs');
|
|
9
9
|
const { detectAndCountFeedback } = feedback;
|
|
10
10
|
|
|
11
|
-
export async function prepareFeedbackAndTimestamps({ prNumber, branchName: _branchName, owner, repo, issueNumber, isContinueMode: _isContinueMode, mergeStateStatus: _mergeStateStatus, prState: _prState, argv: _argv, log, formatAligned, cleanErrorMessage: _cleanErrorMessage, $ }) {
|
|
11
|
+
export async function prepareFeedbackAndTimestamps({ tempDir = null, prNumber, branchName: _branchName, owner, repo, issueNumber, isContinueMode: _isContinueMode, mergeStateStatus: _mergeStateStatus, prState: _prState, argv: _argv, log, formatAligned, cleanErrorMessage: _cleanErrorMessage, $ }) {
|
|
12
12
|
// Count new comments and detect feedback
|
|
13
13
|
let { feedbackLines } = await detectAndCountFeedback({
|
|
14
14
|
prNumber,
|
|
@@ -25,6 +25,7 @@ export async function prepareFeedbackAndTimestamps({ prNumber, branchName: _bran
|
|
|
25
25
|
formatAligned,
|
|
26
26
|
cleanErrorMessage: _cleanErrorMessage,
|
|
27
27
|
$,
|
|
28
|
+
repositoryPath: tempDir,
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
// Get timestamps from GitHub servers before executing the command
|
package/src/solve.watch.lib.mjs
CHANGED