opencode-pilot 0.11.0 → 0.11.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/package.json +1 -1
- package/service/poller.js +58 -15
- package/service/utils.js +28 -2
- package/test/unit/poller.test.js +23 -0
- package/test/unit/utils.test.js +71 -0
package/package.json
CHANGED
package/service/poller.js
CHANGED
|
@@ -273,17 +273,53 @@ export async function pollGenericSource(source, options = {}) {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Fetch issue comments using gh CLI
|
|
278
|
+
*
|
|
279
|
+
* The GitHub MCP server doesn't have a tool to list issue comments,
|
|
280
|
+
* so we use gh CLI directly. This fetches the conversation thread
|
|
281
|
+
* where bots like Linear post their comments.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} owner - Repository owner
|
|
284
|
+
* @param {string} repo - Repository name
|
|
285
|
+
* @param {number} number - Issue/PR number
|
|
286
|
+
* @param {number} timeout - Timeout in ms
|
|
287
|
+
* @returns {Promise<Array>} Array of comment objects
|
|
288
|
+
*/
|
|
289
|
+
async function fetchIssueCommentsViaCli(owner, repo, number, timeout) {
|
|
290
|
+
const { exec } = await import('child_process');
|
|
291
|
+
const { promisify } = await import('util');
|
|
292
|
+
const execAsync = promisify(exec);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const { stdout } = await Promise.race([
|
|
296
|
+
execAsync(`gh api repos/${owner}/${repo}/issues/${number}/comments`),
|
|
297
|
+
createTimeout(timeout, "gh api call"),
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
const comments = JSON.parse(stdout);
|
|
301
|
+
return Array.isArray(comments) ? comments : [];
|
|
302
|
+
} catch (err) {
|
|
303
|
+
// gh CLI might not be available or authenticated
|
|
304
|
+
console.error(`[poller] Error fetching issue comments via gh: ${err.message}`);
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
276
309
|
/**
|
|
277
310
|
* Fetch comments for a GitHub issue/PR and enrich the item
|
|
278
311
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
312
|
+
* Fetches BOTH types of comments:
|
|
313
|
+
* 1. PR review comments (inline code comments) via MCP github_get_pull_request_comments
|
|
314
|
+
* 2. Issue comments (conversation thread) via gh CLI
|
|
315
|
+
*
|
|
316
|
+
* This is necessary because bots like Linear post to issue comments, not PR review comments.
|
|
281
317
|
*
|
|
282
318
|
* @param {object} item - Item with owner, repo_short, and number fields
|
|
283
319
|
* @param {object} [options] - Options
|
|
284
320
|
* @param {number} [options.timeout] - Timeout in ms (default: 30000)
|
|
285
321
|
* @param {string} [options.opencodeConfigPath] - Path to opencode.json for MCP config
|
|
286
|
-
* @returns {Promise<Array>} Array of comment objects
|
|
322
|
+
* @returns {Promise<Array>} Array of comment objects (merged from both endpoints)
|
|
287
323
|
*/
|
|
288
324
|
export async function fetchGitHubComments(item, options = {}) {
|
|
289
325
|
const timeout = options.timeout || DEFAULT_MCP_TIMEOUT;
|
|
@@ -322,24 +358,31 @@ export async function fetchGitHubComments(item, options = {}) {
|
|
|
322
358
|
createTimeout(timeout, "MCP connection for comments"),
|
|
323
359
|
]);
|
|
324
360
|
|
|
325
|
-
//
|
|
326
|
-
const
|
|
361
|
+
// Fetch both PR review comments (via MCP) AND issue comments (via gh CLI) in parallel
|
|
362
|
+
const [prCommentsResult, issueComments] = await Promise.all([
|
|
363
|
+
// PR review comments via MCP (may not be available on all MCP servers)
|
|
327
364
|
client.callTool({
|
|
328
365
|
name: "github_get_pull_request_comments",
|
|
329
366
|
arguments: { owner, repo, pull_number: number }
|
|
330
|
-
}),
|
|
331
|
-
|
|
367
|
+
}).catch(() => null), // Gracefully handle if tool doesn't exist
|
|
368
|
+
// Issue comments via gh CLI (conversation thread where Linear bot posts)
|
|
369
|
+
fetchIssueCommentsViaCli(owner, repo, number, timeout),
|
|
332
370
|
]);
|
|
333
371
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
372
|
+
// Parse PR review comments
|
|
373
|
+
let prComments = [];
|
|
374
|
+
const prText = prCommentsResult?.content?.[0]?.text;
|
|
375
|
+
if (prText) {
|
|
376
|
+
try {
|
|
377
|
+
const parsed = JSON.parse(prText);
|
|
378
|
+
prComments = Array.isArray(parsed) ? parsed : [];
|
|
379
|
+
} catch {
|
|
380
|
+
// Ignore parse errors
|
|
381
|
+
}
|
|
342
382
|
}
|
|
383
|
+
|
|
384
|
+
// Return merged comments from both sources
|
|
385
|
+
return [...prComments, ...issueComments];
|
|
343
386
|
} catch (err) {
|
|
344
387
|
console.error(`[poller] Error fetching comments: ${err.message}`);
|
|
345
388
|
return [];
|
package/service/utils.js
CHANGED
|
@@ -20,6 +20,26 @@ export function getNestedValue(obj, path) {
|
|
|
20
20
|
return value;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Check if a comment/review is an approval-only (no actionable feedback)
|
|
25
|
+
*
|
|
26
|
+
* PR reviews have a state field (APPROVED, CHANGES_REQUESTED, COMMENTED).
|
|
27
|
+
* An approval without substantive body text doesn't require action from the author.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} comment - Comment or review object with optional state and body
|
|
30
|
+
* @returns {boolean} True if this is a pure approval with no actionable feedback
|
|
31
|
+
*/
|
|
32
|
+
export function isApprovalOnly(comment) {
|
|
33
|
+
// Only applies to PR reviews with APPROVED state
|
|
34
|
+
if (comment.state !== 'APPROVED') return false;
|
|
35
|
+
|
|
36
|
+
// If there's substantive body text, it might contain feedback
|
|
37
|
+
const body = comment.body || '';
|
|
38
|
+
if (body.trim().length > 0) return false;
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
/**
|
|
24
44
|
* Check if a username represents a bot account
|
|
25
45
|
*
|
|
@@ -50,9 +70,12 @@ export function isBot(username, type) {
|
|
|
50
70
|
* Used to filter out PRs where only bots have commented, since those don't
|
|
51
71
|
* require the author's attention for human feedback.
|
|
52
72
|
*
|
|
73
|
+
* Also skips approval-only reviews (APPROVED state with no body text) since
|
|
74
|
+
* approvals don't require action from the author.
|
|
75
|
+
*
|
|
53
76
|
* @param {Array} comments - Array of comment objects with user.login and user.type
|
|
54
77
|
* @param {string} authorUsername - Username of the PR/issue author
|
|
55
|
-
* @returns {boolean} True if there's at least one non-bot, non-author comment
|
|
78
|
+
* @returns {boolean} True if there's at least one non-bot, non-author, actionable comment
|
|
56
79
|
*/
|
|
57
80
|
export function hasNonBotFeedback(comments, authorUsername) {
|
|
58
81
|
// Handle null/undefined/empty
|
|
@@ -75,7 +98,10 @@ export function hasNonBotFeedback(comments, authorUsername) {
|
|
|
75
98
|
// Skip if it's the author themselves
|
|
76
99
|
if (authorLower && username?.toLowerCase() === authorLower) continue;
|
|
77
100
|
|
|
78
|
-
//
|
|
101
|
+
// Skip approval-only reviews (no actionable feedback)
|
|
102
|
+
if (isApprovalOnly(comment)) continue;
|
|
103
|
+
|
|
104
|
+
// Found a non-bot, non-author, actionable comment
|
|
79
105
|
return true;
|
|
80
106
|
}
|
|
81
107
|
|
package/test/unit/poller.test.js
CHANGED
|
@@ -734,6 +734,29 @@ describe('poller.js', () => {
|
|
|
734
734
|
});
|
|
735
735
|
});
|
|
736
736
|
|
|
737
|
+
describe('fetchGitHubComments', () => {
|
|
738
|
+
// Note: These tests document the expected behavior
|
|
739
|
+
// Actual MCP calls require mocking which is complex
|
|
740
|
+
|
|
741
|
+
test('should fetch both PR review comments AND issue comments', async () => {
|
|
742
|
+
// The issue: Linear bot posts to issue comments endpoint, not PR review comments
|
|
743
|
+
// PR review comments: GET /repos/{owner}/{repo}/pulls/{pull_number}/comments
|
|
744
|
+
// Issue comments: GET /repos/{owner}/{repo}/issues/{issue_number}/comments
|
|
745
|
+
//
|
|
746
|
+
// For proper bot filtering, we need to check BOTH endpoints
|
|
747
|
+
// Currently only PR review comments are fetched, causing Linear bot
|
|
748
|
+
// comments to be missed (they appear in issue comments)
|
|
749
|
+
|
|
750
|
+
// This test documents the expected behavior - fetchGitHubComments should
|
|
751
|
+
// return comments from both endpoints merged together
|
|
752
|
+
const { fetchGitHubComments } = await import('../../service/poller.js');
|
|
753
|
+
|
|
754
|
+
// Without proper mocking, we can only test the function exists
|
|
755
|
+
// and accepts the right parameters
|
|
756
|
+
assert.strictEqual(typeof fetchGitHubComments, 'function');
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
737
760
|
describe('parseJsonArray', () => {
|
|
738
761
|
test('parses direct array response', async () => {
|
|
739
762
|
const { parseJsonArray } = await import('../../service/poller.js');
|
package/test/unit/utils.test.js
CHANGED
|
@@ -100,6 +100,77 @@ describe('utils.js', () => {
|
|
|
100
100
|
|
|
101
101
|
assert.strictEqual(hasNonBotFeedback(comments, 'contributor'), true);
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
test('returns false when only approval-only reviews (no feedback body)', async () => {
|
|
105
|
+
const { hasNonBotFeedback } = await import('../../service/utils.js');
|
|
106
|
+
|
|
107
|
+
// PR reviews with APPROVED state but no body should not trigger feedback
|
|
108
|
+
const comments = [
|
|
109
|
+
{ user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
|
|
110
|
+
{ user: { login: 'reviewer', type: 'User' }, state: 'APPROVED', body: '' },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(hasNonBotFeedback(comments, 'author'), false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('returns true when approval includes feedback body', async () => {
|
|
117
|
+
const { hasNonBotFeedback } = await import('../../service/utils.js');
|
|
118
|
+
|
|
119
|
+
// If someone approves but leaves feedback, we should consider it actionable
|
|
120
|
+
const comments = [
|
|
121
|
+
{ user: { login: 'reviewer', type: 'User' }, state: 'APPROVED', body: 'LGTM but consider adding a test for edge cases' },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
assert.strictEqual(hasNonBotFeedback(comments, 'author'), true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('returns true for CHANGES_REQUESTED reviews', async () => {
|
|
128
|
+
const { hasNonBotFeedback } = await import('../../service/utils.js');
|
|
129
|
+
|
|
130
|
+
const comments = [
|
|
131
|
+
{ user: { login: 'reviewer', type: 'User' }, state: 'CHANGES_REQUESTED', body: 'Please fix this' },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
assert.strictEqual(hasNonBotFeedback(comments, 'author'), true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('isApprovalOnly', () => {
|
|
139
|
+
test('returns true for APPROVED state with no body', async () => {
|
|
140
|
+
const { isApprovalOnly } = await import('../../service/utils.js');
|
|
141
|
+
|
|
142
|
+
assert.strictEqual(isApprovalOnly({ state: 'APPROVED' }), true);
|
|
143
|
+
assert.strictEqual(isApprovalOnly({ state: 'APPROVED', body: '' }), true);
|
|
144
|
+
assert.strictEqual(isApprovalOnly({ state: 'APPROVED', body: null }), true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('returns false for APPROVED with substantive body', async () => {
|
|
148
|
+
const { isApprovalOnly } = await import('../../service/utils.js');
|
|
149
|
+
|
|
150
|
+
// If someone approves but leaves feedback, we should still consider it feedback
|
|
151
|
+
assert.strictEqual(isApprovalOnly({ state: 'APPROVED', body: 'LGTM but consider renaming this function' }), false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('returns false for CHANGES_REQUESTED', async () => {
|
|
155
|
+
const { isApprovalOnly } = await import('../../service/utils.js');
|
|
156
|
+
|
|
157
|
+
assert.strictEqual(isApprovalOnly({ state: 'CHANGES_REQUESTED', body: 'Please fix this' }), false);
|
|
158
|
+
assert.strictEqual(isApprovalOnly({ state: 'CHANGES_REQUESTED' }), false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('returns false for COMMENTED state', async () => {
|
|
162
|
+
const { isApprovalOnly } = await import('../../service/utils.js');
|
|
163
|
+
|
|
164
|
+
assert.strictEqual(isApprovalOnly({ state: 'COMMENTED', body: 'This looks good' }), false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('returns false for regular comments without state', async () => {
|
|
168
|
+
const { isApprovalOnly } = await import('../../service/utils.js');
|
|
169
|
+
|
|
170
|
+
// Issue comments don't have state field
|
|
171
|
+
assert.strictEqual(isApprovalOnly({ body: 'Please address this' }), false);
|
|
172
|
+
assert.strictEqual(isApprovalOnly({}), false);
|
|
173
|
+
});
|
|
103
174
|
});
|
|
104
175
|
|
|
105
176
|
describe('getNestedValue', () => {
|