opencode-pilot 0.17.1 → 0.18.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/README.md +4 -1
- package/examples/config.yaml +7 -2
- package/package.json +1 -1
- package/service/poll-service.js +13 -1
- package/service/poller.js +110 -0
- package/service/presets/github.yaml +24 -1
- package/service/readiness.js +91 -0
- package/test/unit/poller.test.js +112 -0
- package/test/unit/readiness.test.js +289 -0
- package/test/unit/repo-config.test.js +27 -0
package/README.md
CHANGED
|
@@ -65,9 +65,12 @@ Three ways to configure sources, from simplest to most flexible:
|
|
|
65
65
|
|
|
66
66
|
- `github/my-issues` - Issues assigned to me
|
|
67
67
|
- `github/review-requests` - PRs needing my review
|
|
68
|
-
- `github/my-prs-feedback` - My PRs with
|
|
68
|
+
- `github/my-prs-feedback` - My PRs with comments/feedback (simple trigger)
|
|
69
|
+
- `github/my-prs-attention` - My PRs needing attention (conflicts OR feedback, with dynamic labeling)
|
|
69
70
|
- `linear/my-issues` - Linear tickets (requires `teamId`, `assigneeId`)
|
|
70
71
|
|
|
72
|
+
**Tip:** Use `my-prs-attention` instead of `my-prs-feedback` if you also want to detect merge conflicts. The session name will indicate which condition(s) triggered it: "Conflicts: {title}", "Feedback: {title}", or "Conflicts+Feedback: {title}".
|
|
73
|
+
|
|
71
74
|
### Prompt Templates
|
|
72
75
|
|
|
73
76
|
Create prompt templates as markdown files in `~/.config/opencode/pilot/templates/`. Templates support placeholders like `{title}`, `{body}`, `{number}`, `{html_url}`, etc.
|
package/examples/config.yaml
CHANGED
|
@@ -27,11 +27,16 @@ sources:
|
|
|
27
27
|
|
|
28
28
|
- preset: github/review-requests
|
|
29
29
|
|
|
30
|
-
-
|
|
30
|
+
# PRs needing attention (conflicts OR feedback) - recommended over my-prs-feedback
|
|
31
|
+
# Session names dynamically indicate the condition: "Conflicts: ...", "Feedback: ...", or "Conflicts+Feedback: ..."
|
|
32
|
+
- preset: github/my-prs-attention
|
|
31
33
|
repos:
|
|
32
34
|
- myorg/backend
|
|
33
35
|
- myorg/frontend
|
|
34
36
|
|
|
37
|
+
# Alternative: Simple feedback-only trigger (use my-prs-attention for conflicts too)
|
|
38
|
+
# - preset: github/my-prs-feedback
|
|
39
|
+
|
|
35
40
|
# Linear (requires teamId and assigneeId)
|
|
36
41
|
- preset: linear/my-issues
|
|
37
42
|
args:
|
|
@@ -87,4 +92,4 @@ sources:
|
|
|
87
92
|
# ttl_days: 30
|
|
88
93
|
|
|
89
94
|
# Available presets: github/my-issues, github/review-requests,
|
|
90
|
-
# github/my-prs-feedback, linear/my-issues
|
|
95
|
+
# github/my-prs-feedback, github/my-prs-attention, linear/my-issues
|
package/package.json
CHANGED
package/service/poll-service.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { loadRepoConfig, getRepoConfig, getAllSources, getToolProviderConfig, resolveRepoForItem, getCleanupTtlDays, getStartupDelay } from "./repo-config.js";
|
|
13
|
-
import { createPoller, pollGenericSource, enrichItemsWithComments } from "./poller.js";
|
|
13
|
+
import { createPoller, pollGenericSource, enrichItemsWithComments, enrichItemsWithMergeable, computeAttentionLabels } from "./poller.js";
|
|
14
14
|
import { evaluateReadiness, sortByPriority } from "./readiness.js";
|
|
15
15
|
import { executeAction, buildCommand } from "./actions.js";
|
|
16
16
|
import { debug } from "./logger.js";
|
|
@@ -133,6 +133,18 @@ export async function pollOnce(options = {}) {
|
|
|
133
133
|
items = await enrichItemsWithComments(items, source);
|
|
134
134
|
debug(`Enriched ${items.length} items with comments for bot filtering`);
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
// Enrich items with mergeable status for conflict detection if configured
|
|
138
|
+
if (source.enrich_mergeable) {
|
|
139
|
+
items = await enrichItemsWithMergeable(items, source);
|
|
140
|
+
debug(`Enriched ${items.length} items with mergeable status`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Compute attention labels if both enrichments are present (for my-prs-attention)
|
|
144
|
+
if (source.enrich_mergeable && source.filter_bot_comments) {
|
|
145
|
+
items = computeAttentionLabels(items, source);
|
|
146
|
+
debug(`Computed attention labels for ${items.length} items`);
|
|
147
|
+
}
|
|
136
148
|
} catch (err) {
|
|
137
149
|
console.error(`[poll] Error fetching from ${sourceName}: ${err.message}`);
|
|
138
150
|
continue;
|
package/service/poller.js
CHANGED
|
@@ -533,6 +533,116 @@ export async function enrichItemsWithComments(items, source, options = {}) {
|
|
|
533
533
|
return enrichedItems;
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Fetch mergeable status for a PR via gh CLI
|
|
538
|
+
*
|
|
539
|
+
* @param {string} owner - Repository owner
|
|
540
|
+
* @param {string} repo - Repository name
|
|
541
|
+
* @param {number} number - PR number
|
|
542
|
+
* @param {number} timeout - Timeout in ms
|
|
543
|
+
* @returns {Promise<string|null>} Mergeable status ("MERGEABLE", "CONFLICTING", "UNKNOWN") or null on error
|
|
544
|
+
*/
|
|
545
|
+
async function fetchMergeableStatus(owner, repo, number, timeout) {
|
|
546
|
+
const { exec } = await import('child_process');
|
|
547
|
+
const { promisify } = await import('util');
|
|
548
|
+
const execAsync = promisify(exec);
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const { stdout } = await Promise.race([
|
|
552
|
+
execAsync(`gh pr view ${number} -R ${owner}/${repo} --json mergeable --jq .mergeable`),
|
|
553
|
+
createTimeout(timeout, "gh pr view"),
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
const status = stdout.trim();
|
|
557
|
+
return status || null;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
console.error(`[poller] Error fetching mergeable status for ${owner}/${repo}#${number}: ${err.message}`);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Enrich items with mergeable status for conflict detection
|
|
566
|
+
*
|
|
567
|
+
* For items from sources with enrich_mergeable: true, fetches mergeable status
|
|
568
|
+
* via gh CLI and attaches it as _mergeable field for readiness evaluation.
|
|
569
|
+
*
|
|
570
|
+
* @param {Array} items - Items to enrich
|
|
571
|
+
* @param {object} source - Source configuration with optional enrich_mergeable
|
|
572
|
+
* @param {object} [options] - Options
|
|
573
|
+
* @param {number} [options.timeout] - Timeout in ms (default: 30000)
|
|
574
|
+
* @returns {Promise<Array>} Items with _mergeable field added
|
|
575
|
+
*/
|
|
576
|
+
export async function enrichItemsWithMergeable(items, source, options = {}) {
|
|
577
|
+
// Skip if not configured
|
|
578
|
+
if (!source.enrich_mergeable) {
|
|
579
|
+
return items;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const timeout = options.timeout || DEFAULT_MCP_TIMEOUT;
|
|
583
|
+
|
|
584
|
+
// Fetch mergeable status for each item
|
|
585
|
+
const enrichedItems = [];
|
|
586
|
+
for (const item of items) {
|
|
587
|
+
// Extract owner/repo from item
|
|
588
|
+
const fullName = item.repository_full_name || item.repository?.nameWithOwner;
|
|
589
|
+
if (!fullName || !item.number) {
|
|
590
|
+
enrichedItems.push(item);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const [owner, repo] = fullName.split("/");
|
|
595
|
+
const mergeable = await fetchMergeableStatus(owner, repo, item.number, timeout);
|
|
596
|
+
enrichedItems.push({ ...item, _mergeable: mergeable });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return enrichedItems;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Compute attention label from enriched item conditions
|
|
604
|
+
*
|
|
605
|
+
* Examines _mergeable and _comments fields to determine what needs attention.
|
|
606
|
+
* Sets _attention_label (for session name) and _has_attention (for readiness).
|
|
607
|
+
*
|
|
608
|
+
* @param {Array} items - Items enriched with _mergeable and/or _comments
|
|
609
|
+
* @param {object} source - Source configuration
|
|
610
|
+
* @returns {Array} Items with _attention_label and _has_attention added
|
|
611
|
+
*/
|
|
612
|
+
export function computeAttentionLabels(items, source) {
|
|
613
|
+
return items.map(item => {
|
|
614
|
+
const reasons = [];
|
|
615
|
+
|
|
616
|
+
// Check for merge conflicts
|
|
617
|
+
if (item._mergeable === 'CONFLICTING') {
|
|
618
|
+
reasons.push('Conflicts');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Check for human feedback (non-bot, non-author comments)
|
|
622
|
+
if (item._comments && item._comments.length > 0) {
|
|
623
|
+
const authorUsername = item.user?.login || item.author?.login;
|
|
624
|
+
const hasHumanFeedback = item._comments.some(comment => {
|
|
625
|
+
const commenter = comment.user?.login || comment.author?.login;
|
|
626
|
+
const isBot = commenter?.includes('[bot]') || comment.user?.type === 'Bot';
|
|
627
|
+
const isAuthor = commenter === authorUsername;
|
|
628
|
+
return !isBot && !isAuthor;
|
|
629
|
+
});
|
|
630
|
+
if (hasHumanFeedback) {
|
|
631
|
+
reasons.push('Feedback');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Build label: "Conflicts", "Feedback", or "Conflicts+Feedback"
|
|
636
|
+
const label = reasons.length > 0 ? reasons.join('+') : 'PR';
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
...item,
|
|
640
|
+
_attention_label: label,
|
|
641
|
+
_has_attention: reasons.length > 0,
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
536
646
|
/**
|
|
537
647
|
* Create a poller instance with state tracking
|
|
538
648
|
*
|
|
@@ -41,7 +41,7 @@ my-prs-feedback:
|
|
|
41
41
|
name: my-prs-feedback
|
|
42
42
|
tool:
|
|
43
43
|
# comments:>0 filter ensures only PRs with feedback are returned
|
|
44
|
-
command: ["gh", "search", "prs", "--author=@me", "--state=open", "comments:>0", "--json", "number,title,url,repository,state,body,updatedAt"]
|
|
44
|
+
command: ["gh", "search", "prs", "--author=@me", "--state=open", "comments:>0", "--json", "number,title,url,repository,state,body,updatedAt,commentsCount"]
|
|
45
45
|
item:
|
|
46
46
|
id: "{url}"
|
|
47
47
|
repo: "{repository.nameWithOwner}"
|
|
@@ -53,3 +53,26 @@ my-prs-feedback:
|
|
|
53
53
|
reprocess_on:
|
|
54
54
|
- state
|
|
55
55
|
- updatedAt
|
|
56
|
+
|
|
57
|
+
my-prs-attention:
|
|
58
|
+
name: my-prs-attention
|
|
59
|
+
tool:
|
|
60
|
+
# Get all open PRs authored by me - we filter by conditions after enrichment
|
|
61
|
+
command: ["gh", "search", "prs", "--author=@me", "--state=open", "--json", "number,title,url,repository,state,body,updatedAt,commentsCount"]
|
|
62
|
+
item:
|
|
63
|
+
id: "{url}"
|
|
64
|
+
repo: "{repository.nameWithOwner}"
|
|
65
|
+
prompt: review-feedback
|
|
66
|
+
session:
|
|
67
|
+
# Dynamic name showing which conditions triggered (set by enrichment)
|
|
68
|
+
name: "{_attention_label}: {title}"
|
|
69
|
+
# Enrich with both mergeable status and comments to detect all attention conditions
|
|
70
|
+
enrich_mergeable: true
|
|
71
|
+
filter_bot_comments: true
|
|
72
|
+
readiness:
|
|
73
|
+
# Require at least one attention condition (conflicts or human feedback)
|
|
74
|
+
require_attention: true
|
|
75
|
+
# Reprocess when PR is updated
|
|
76
|
+
reprocess_on:
|
|
77
|
+
- state
|
|
78
|
+
- updatedAt
|
package/service/readiness.js
CHANGED
|
@@ -213,6 +213,77 @@ export function checkBotComments(item, config) {
|
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Check if a PR has merge conflicts
|
|
218
|
+
*
|
|
219
|
+
* This check is only applied when the item has been enriched with `_mergeable`
|
|
220
|
+
* (the mergeable status from GitHub: "MERGEABLE", "CONFLICTING", or "UNKNOWN").
|
|
221
|
+
* Items without `_mergeable` are considered ready (check is skipped).
|
|
222
|
+
*
|
|
223
|
+
* Used by the my-prs-conflicts source to filter to only PRs with conflicts.
|
|
224
|
+
*
|
|
225
|
+
* @param {object} item - Item with optional _mergeable field
|
|
226
|
+
* @param {object} config - Repo config with optional readiness.require_conflicts
|
|
227
|
+
* @returns {object} { ready: boolean, reason?: string }
|
|
228
|
+
*/
|
|
229
|
+
export function checkMergeable(item, config) {
|
|
230
|
+
const readinessConfig = config.readiness || {};
|
|
231
|
+
|
|
232
|
+
// Skip check if no _mergeable field (item not enriched)
|
|
233
|
+
if (!item._mergeable) {
|
|
234
|
+
return { ready: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if we require conflicts (for conflict-detection sources)
|
|
238
|
+
if (readinessConfig.require_conflicts) {
|
|
239
|
+
if (item._mergeable === "CONFLICTING") {
|
|
240
|
+
return { ready: true };
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
ready: false,
|
|
244
|
+
reason: `PR is ${item._mergeable}, not CONFLICTING`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Default: allow any mergeable status
|
|
249
|
+
return { ready: true };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if a PR needs attention (has conflicts OR human feedback)
|
|
254
|
+
*
|
|
255
|
+
* This check uses the _has_attention field computed by computeAttentionLabels().
|
|
256
|
+
* Items without _has_attention are considered ready (check is skipped).
|
|
257
|
+
*
|
|
258
|
+
* Used by the my-prs-attention source to filter to PRs needing action.
|
|
259
|
+
*
|
|
260
|
+
* @param {object} item - Item with optional _has_attention field
|
|
261
|
+
* @param {object} config - Repo config with optional readiness.require_attention
|
|
262
|
+
* @returns {object} { ready: boolean, reason?: string }
|
|
263
|
+
*/
|
|
264
|
+
export function checkAttention(item, config) {
|
|
265
|
+
const readinessConfig = config.readiness || {};
|
|
266
|
+
|
|
267
|
+
// Skip check if require_attention not configured
|
|
268
|
+
if (!readinessConfig.require_attention) {
|
|
269
|
+
return { ready: true };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Skip check if _has_attention not computed (item not enriched)
|
|
273
|
+
if (item._has_attention === undefined) {
|
|
274
|
+
return { ready: true };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (item._has_attention) {
|
|
278
|
+
return { ready: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
ready: false,
|
|
283
|
+
reason: "PR has no conflicts and no human feedback - no attention needed",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
216
287
|
/**
|
|
217
288
|
* Calculate priority score for an issue
|
|
218
289
|
* @param {object} issue - Issue with labels and created_at
|
|
@@ -286,6 +357,26 @@ export function evaluateReadiness(issue, config) {
|
|
|
286
357
|
};
|
|
287
358
|
}
|
|
288
359
|
|
|
360
|
+
// Check mergeable status (for PRs enriched with _mergeable)
|
|
361
|
+
const mergeableResult = checkMergeable(issue, config);
|
|
362
|
+
if (!mergeableResult.ready) {
|
|
363
|
+
return {
|
|
364
|
+
ready: false,
|
|
365
|
+
reason: mergeableResult.reason,
|
|
366
|
+
priority: 0,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check attention status (for PRs needing conflicts OR feedback)
|
|
371
|
+
const attentionResult = checkAttention(issue, config);
|
|
372
|
+
if (!attentionResult.ready) {
|
|
373
|
+
return {
|
|
374
|
+
ready: false,
|
|
375
|
+
reason: attentionResult.reason,
|
|
376
|
+
priority: 0,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
289
380
|
// Check required field values
|
|
290
381
|
const fieldsResult = checkFields(issue, config);
|
|
291
382
|
if (!fieldsResult.ready) {
|
package/test/unit/poller.test.js
CHANGED
|
@@ -902,4 +902,116 @@ describe('poller.js', () => {
|
|
|
902
902
|
assert.strictEqual(transformed[0].title, 'First');
|
|
903
903
|
});
|
|
904
904
|
});
|
|
905
|
+
|
|
906
|
+
describe('computeAttentionLabels', () => {
|
|
907
|
+
test('labels PR with conflicts only', async () => {
|
|
908
|
+
const { computeAttentionLabels } = await import('../../service/poller.js');
|
|
909
|
+
|
|
910
|
+
const items = [{
|
|
911
|
+
number: 123,
|
|
912
|
+
title: 'Test PR',
|
|
913
|
+
_mergeable: 'CONFLICTING',
|
|
914
|
+
_comments: []
|
|
915
|
+
}];
|
|
916
|
+
|
|
917
|
+
const result = computeAttentionLabels(items, {});
|
|
918
|
+
|
|
919
|
+
assert.strictEqual(result[0]._attention_label, 'Conflicts');
|
|
920
|
+
assert.strictEqual(result[0]._has_attention, true);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
test('labels PR with human feedback only', async () => {
|
|
924
|
+
const { computeAttentionLabels } = await import('../../service/poller.js');
|
|
925
|
+
|
|
926
|
+
const items = [{
|
|
927
|
+
number: 123,
|
|
928
|
+
title: 'Test PR',
|
|
929
|
+
user: { login: 'author' },
|
|
930
|
+
_mergeable: 'MERGEABLE',
|
|
931
|
+
_comments: [
|
|
932
|
+
{ user: { login: 'reviewer', type: 'User' }, body: 'Please fix' }
|
|
933
|
+
]
|
|
934
|
+
}];
|
|
935
|
+
|
|
936
|
+
const result = computeAttentionLabels(items, {});
|
|
937
|
+
|
|
938
|
+
assert.strictEqual(result[0]._attention_label, 'Feedback');
|
|
939
|
+
assert.strictEqual(result[0]._has_attention, true);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test('labels PR with both conflicts and feedback', async () => {
|
|
943
|
+
const { computeAttentionLabels } = await import('../../service/poller.js');
|
|
944
|
+
|
|
945
|
+
const items = [{
|
|
946
|
+
number: 123,
|
|
947
|
+
title: 'Test PR',
|
|
948
|
+
user: { login: 'author' },
|
|
949
|
+
_mergeable: 'CONFLICTING',
|
|
950
|
+
_comments: [
|
|
951
|
+
{ user: { login: 'reviewer', type: 'User' }, body: 'Please fix' }
|
|
952
|
+
]
|
|
953
|
+
}];
|
|
954
|
+
|
|
955
|
+
const result = computeAttentionLabels(items, {});
|
|
956
|
+
|
|
957
|
+
assert.strictEqual(result[0]._attention_label, 'Conflicts+Feedback');
|
|
958
|
+
assert.strictEqual(result[0]._has_attention, true);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
test('labels PR with no attention conditions', async () => {
|
|
962
|
+
const { computeAttentionLabels } = await import('../../service/poller.js');
|
|
963
|
+
|
|
964
|
+
const items = [{
|
|
965
|
+
number: 123,
|
|
966
|
+
title: 'Test PR',
|
|
967
|
+
user: { login: 'author' },
|
|
968
|
+
_mergeable: 'MERGEABLE',
|
|
969
|
+
_comments: []
|
|
970
|
+
}];
|
|
971
|
+
|
|
972
|
+
const result = computeAttentionLabels(items, {});
|
|
973
|
+
|
|
974
|
+
assert.strictEqual(result[0]._attention_label, 'PR');
|
|
975
|
+
assert.strictEqual(result[0]._has_attention, false);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
test('ignores bot comments when computing feedback', async () => {
|
|
979
|
+
const { computeAttentionLabels } = await import('../../service/poller.js');
|
|
980
|
+
|
|
981
|
+
const items = [{
|
|
982
|
+
number: 123,
|
|
983
|
+
title: 'Test PR',
|
|
984
|
+
user: { login: 'author' },
|
|
985
|
+
_mergeable: 'MERGEABLE',
|
|
986
|
+
_comments: [
|
|
987
|
+
{ user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
|
|
988
|
+
{ user: { login: 'codecov[bot]', type: 'Bot' }, body: 'Coverage report' }
|
|
989
|
+
]
|
|
990
|
+
}];
|
|
991
|
+
|
|
992
|
+
const result = computeAttentionLabels(items, {});
|
|
993
|
+
|
|
994
|
+
assert.strictEqual(result[0]._attention_label, 'PR');
|
|
995
|
+
assert.strictEqual(result[0]._has_attention, false);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test('ignores author comments when computing feedback', async () => {
|
|
999
|
+
const { computeAttentionLabels } = await import('../../service/poller.js');
|
|
1000
|
+
|
|
1001
|
+
const items = [{
|
|
1002
|
+
number: 123,
|
|
1003
|
+
title: 'Test PR',
|
|
1004
|
+
user: { login: 'author' },
|
|
1005
|
+
_mergeable: 'MERGEABLE',
|
|
1006
|
+
_comments: [
|
|
1007
|
+
{ user: { login: 'author', type: 'User' }, body: 'Added screenshots' }
|
|
1008
|
+
]
|
|
1009
|
+
}];
|
|
1010
|
+
|
|
1011
|
+
const result = computeAttentionLabels(items, {});
|
|
1012
|
+
|
|
1013
|
+
assert.strictEqual(result[0]._attention_label, 'PR');
|
|
1014
|
+
assert.strictEqual(result[0]._has_attention, false);
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
905
1017
|
});
|
|
@@ -387,4 +387,293 @@ describe('readiness.js', () => {
|
|
|
387
387
|
assert.strictEqual(result.ready, true);
|
|
388
388
|
});
|
|
389
389
|
});
|
|
390
|
+
|
|
391
|
+
describe('checkMergeable', () => {
|
|
392
|
+
test('returns ready when no _mergeable field (skip check)', async () => {
|
|
393
|
+
const { checkMergeable } = await import('../../service/readiness.js');
|
|
394
|
+
|
|
395
|
+
const pr = {
|
|
396
|
+
number: 123,
|
|
397
|
+
title: 'Test PR'
|
|
398
|
+
};
|
|
399
|
+
const config = {};
|
|
400
|
+
|
|
401
|
+
const result = checkMergeable(pr, config);
|
|
402
|
+
|
|
403
|
+
assert.strictEqual(result.ready, true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('returns ready when PR has conflicts and require_conflicts is true', async () => {
|
|
407
|
+
const { checkMergeable } = await import('../../service/readiness.js');
|
|
408
|
+
|
|
409
|
+
const pr = {
|
|
410
|
+
number: 123,
|
|
411
|
+
title: 'Test PR',
|
|
412
|
+
_mergeable: 'CONFLICTING'
|
|
413
|
+
};
|
|
414
|
+
const config = {
|
|
415
|
+
readiness: {
|
|
416
|
+
require_conflicts: true
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const result = checkMergeable(pr, config);
|
|
421
|
+
|
|
422
|
+
assert.strictEqual(result.ready, true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('returns not ready when PR is mergeable and require_conflicts is true', async () => {
|
|
426
|
+
const { checkMergeable } = await import('../../service/readiness.js');
|
|
427
|
+
|
|
428
|
+
const pr = {
|
|
429
|
+
number: 123,
|
|
430
|
+
title: 'Test PR',
|
|
431
|
+
_mergeable: 'MERGEABLE'
|
|
432
|
+
};
|
|
433
|
+
const config = {
|
|
434
|
+
readiness: {
|
|
435
|
+
require_conflicts: true
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const result = checkMergeable(pr, config);
|
|
440
|
+
|
|
441
|
+
assert.strictEqual(result.ready, false);
|
|
442
|
+
assert.ok(result.reason.includes('MERGEABLE'));
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('returns not ready when PR status is UNKNOWN and require_conflicts is true', async () => {
|
|
446
|
+
const { checkMergeable } = await import('../../service/readiness.js');
|
|
447
|
+
|
|
448
|
+
const pr = {
|
|
449
|
+
number: 123,
|
|
450
|
+
title: 'Test PR',
|
|
451
|
+
_mergeable: 'UNKNOWN'
|
|
452
|
+
};
|
|
453
|
+
const config = {
|
|
454
|
+
readiness: {
|
|
455
|
+
require_conflicts: true
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const result = checkMergeable(pr, config);
|
|
460
|
+
|
|
461
|
+
assert.strictEqual(result.ready, false);
|
|
462
|
+
assert.ok(result.reason.includes('UNKNOWN'));
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('returns ready for any mergeable status when require_conflicts is not set', async () => {
|
|
466
|
+
const { checkMergeable } = await import('../../service/readiness.js');
|
|
467
|
+
|
|
468
|
+
const pr = {
|
|
469
|
+
number: 123,
|
|
470
|
+
title: 'Test PR',
|
|
471
|
+
_mergeable: 'CONFLICTING'
|
|
472
|
+
};
|
|
473
|
+
const config = {};
|
|
474
|
+
|
|
475
|
+
const result = checkMergeable(pr, config);
|
|
476
|
+
|
|
477
|
+
assert.strictEqual(result.ready, true);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe('evaluateReadiness with mergeable', () => {
|
|
482
|
+
test('checks mergeable when _mergeable is present with require_conflicts', async () => {
|
|
483
|
+
const { evaluateReadiness } = await import('../../service/readiness.js');
|
|
484
|
+
|
|
485
|
+
const pr = {
|
|
486
|
+
number: 123,
|
|
487
|
+
title: 'Test PR',
|
|
488
|
+
_mergeable: 'MERGEABLE'
|
|
489
|
+
};
|
|
490
|
+
const config = {
|
|
491
|
+
readiness: {
|
|
492
|
+
require_conflicts: true
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const result = evaluateReadiness(pr, config);
|
|
497
|
+
|
|
498
|
+
assert.strictEqual(result.ready, false);
|
|
499
|
+
assert.ok(result.reason.includes('MERGEABLE'));
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('passes when PR has conflicts and require_conflicts is configured', async () => {
|
|
503
|
+
const { evaluateReadiness } = await import('../../service/readiness.js');
|
|
504
|
+
|
|
505
|
+
const pr = {
|
|
506
|
+
number: 123,
|
|
507
|
+
title: 'Test PR',
|
|
508
|
+
_mergeable: 'CONFLICTING'
|
|
509
|
+
};
|
|
510
|
+
const config = {
|
|
511
|
+
readiness: {
|
|
512
|
+
require_conflicts: true
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const result = evaluateReadiness(pr, config);
|
|
517
|
+
|
|
518
|
+
assert.strictEqual(result.ready, true);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
describe('checkAttention', () => {
|
|
523
|
+
test('returns ready when require_attention is not configured', async () => {
|
|
524
|
+
const { checkAttention } = await import('../../service/readiness.js');
|
|
525
|
+
|
|
526
|
+
const pr = {
|
|
527
|
+
number: 123,
|
|
528
|
+
title: 'Test PR',
|
|
529
|
+
_has_attention: false
|
|
530
|
+
};
|
|
531
|
+
const config = {};
|
|
532
|
+
|
|
533
|
+
const result = checkAttention(pr, config);
|
|
534
|
+
|
|
535
|
+
assert.strictEqual(result.ready, true);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('returns ready when _has_attention is not computed', async () => {
|
|
539
|
+
const { checkAttention } = await import('../../service/readiness.js');
|
|
540
|
+
|
|
541
|
+
const pr = {
|
|
542
|
+
number: 123,
|
|
543
|
+
title: 'Test PR'
|
|
544
|
+
};
|
|
545
|
+
const config = {
|
|
546
|
+
readiness: {
|
|
547
|
+
require_attention: true
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const result = checkAttention(pr, config);
|
|
552
|
+
|
|
553
|
+
assert.strictEqual(result.ready, true);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test('returns ready when PR has attention (conflicts)', async () => {
|
|
557
|
+
const { checkAttention } = await import('../../service/readiness.js');
|
|
558
|
+
|
|
559
|
+
const pr = {
|
|
560
|
+
number: 123,
|
|
561
|
+
title: 'Test PR',
|
|
562
|
+
_has_attention: true,
|
|
563
|
+
_attention_label: 'Conflicts'
|
|
564
|
+
};
|
|
565
|
+
const config = {
|
|
566
|
+
readiness: {
|
|
567
|
+
require_attention: true
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const result = checkAttention(pr, config);
|
|
572
|
+
|
|
573
|
+
assert.strictEqual(result.ready, true);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('returns ready when PR has attention (feedback)', async () => {
|
|
577
|
+
const { checkAttention } = await import('../../service/readiness.js');
|
|
578
|
+
|
|
579
|
+
const pr = {
|
|
580
|
+
number: 123,
|
|
581
|
+
title: 'Test PR',
|
|
582
|
+
_has_attention: true,
|
|
583
|
+
_attention_label: 'Feedback'
|
|
584
|
+
};
|
|
585
|
+
const config = {
|
|
586
|
+
readiness: {
|
|
587
|
+
require_attention: true
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const result = checkAttention(pr, config);
|
|
592
|
+
|
|
593
|
+
assert.strictEqual(result.ready, true);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test('returns ready when PR has both conditions', async () => {
|
|
597
|
+
const { checkAttention } = await import('../../service/readiness.js');
|
|
598
|
+
|
|
599
|
+
const pr = {
|
|
600
|
+
number: 123,
|
|
601
|
+
title: 'Test PR',
|
|
602
|
+
_has_attention: true,
|
|
603
|
+
_attention_label: 'Conflicts+Feedback'
|
|
604
|
+
};
|
|
605
|
+
const config = {
|
|
606
|
+
readiness: {
|
|
607
|
+
require_attention: true
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const result = checkAttention(pr, config);
|
|
612
|
+
|
|
613
|
+
assert.strictEqual(result.ready, true);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test('returns not ready when PR has no attention conditions', async () => {
|
|
617
|
+
const { checkAttention } = await import('../../service/readiness.js');
|
|
618
|
+
|
|
619
|
+
const pr = {
|
|
620
|
+
number: 123,
|
|
621
|
+
title: 'Test PR',
|
|
622
|
+
_has_attention: false,
|
|
623
|
+
_attention_label: 'PR'
|
|
624
|
+
};
|
|
625
|
+
const config = {
|
|
626
|
+
readiness: {
|
|
627
|
+
require_attention: true
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const result = checkAttention(pr, config);
|
|
632
|
+
|
|
633
|
+
assert.strictEqual(result.ready, false);
|
|
634
|
+
assert.ok(result.reason.includes('no conflicts'));
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe('evaluateReadiness with attention', () => {
|
|
639
|
+
test('checks attention when require_attention is configured', async () => {
|
|
640
|
+
const { evaluateReadiness } = await import('../../service/readiness.js');
|
|
641
|
+
|
|
642
|
+
const pr = {
|
|
643
|
+
number: 123,
|
|
644
|
+
title: 'Test PR',
|
|
645
|
+
_has_attention: false
|
|
646
|
+
};
|
|
647
|
+
const config = {
|
|
648
|
+
readiness: {
|
|
649
|
+
require_attention: true
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const result = evaluateReadiness(pr, config);
|
|
654
|
+
|
|
655
|
+
assert.strictEqual(result.ready, false);
|
|
656
|
+
assert.ok(result.reason.includes('no attention needed'));
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('passes when PR has attention', async () => {
|
|
660
|
+
const { evaluateReadiness } = await import('../../service/readiness.js');
|
|
661
|
+
|
|
662
|
+
const pr = {
|
|
663
|
+
number: 123,
|
|
664
|
+
title: 'Test PR',
|
|
665
|
+
_has_attention: true,
|
|
666
|
+
_attention_label: 'Conflicts'
|
|
667
|
+
};
|
|
668
|
+
const config = {
|
|
669
|
+
readiness: {
|
|
670
|
+
require_attention: true
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const result = evaluateReadiness(pr, config);
|
|
675
|
+
|
|
676
|
+
assert.strictEqual(result.ready, true);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
390
679
|
});
|
|
@@ -697,6 +697,28 @@ sources:
|
|
|
697
697
|
assert.deepStrictEqual(sources[0].reprocess_on, ['state', 'updatedAt']);
|
|
698
698
|
});
|
|
699
699
|
|
|
700
|
+
test('expands github/my-prs-attention preset', async () => {
|
|
701
|
+
writeFileSync(configPath, `
|
|
702
|
+
sources:
|
|
703
|
+
- preset: github/my-prs-attention
|
|
704
|
+
`);
|
|
705
|
+
|
|
706
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
707
|
+
loadRepoConfig(configPath);
|
|
708
|
+
const sources = getSources();
|
|
709
|
+
|
|
710
|
+
assert.strictEqual(sources[0].name, 'my-prs-attention');
|
|
711
|
+
// GitHub presets now use gh CLI instead of MCP
|
|
712
|
+
assert.ok(sources[0].tool.command.includes('--author=@me'), 'command should include author filter');
|
|
713
|
+
// This preset enables both mergeable and comments enrichment
|
|
714
|
+
assert.strictEqual(sources[0].enrich_mergeable, true);
|
|
715
|
+
assert.strictEqual(sources[0].filter_bot_comments, true);
|
|
716
|
+
// This preset requires attention (conflicts OR human feedback)
|
|
717
|
+
assert.strictEqual(sources[0].readiness.require_attention, true);
|
|
718
|
+
// Session name uses dynamic attention label
|
|
719
|
+
assert.ok(sources[0].session.name.includes('_attention_label'), 'session name should use dynamic label');
|
|
720
|
+
});
|
|
721
|
+
|
|
700
722
|
test('expands linear/my-issues preset with required args', async () => {
|
|
701
723
|
writeFileSync(configPath, `
|
|
702
724
|
sources:
|
|
@@ -753,6 +775,7 @@ sources:
|
|
|
753
775
|
- preset: github/my-issues
|
|
754
776
|
- preset: github/review-requests
|
|
755
777
|
- preset: github/my-prs-feedback
|
|
778
|
+
- preset: github/my-prs-attention
|
|
756
779
|
`);
|
|
757
780
|
|
|
758
781
|
const { loadRepoConfig, getSources, resolveRepoForItem } = await import('../../service/repo-config.js');
|
|
@@ -838,6 +861,7 @@ sources:
|
|
|
838
861
|
- preset: github/my-issues
|
|
839
862
|
- preset: github/review-requests
|
|
840
863
|
- preset: github/my-prs-feedback
|
|
864
|
+
- preset: github/my-prs-attention
|
|
841
865
|
`);
|
|
842
866
|
|
|
843
867
|
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
@@ -852,6 +876,9 @@ sources:
|
|
|
852
876
|
|
|
853
877
|
// my-prs-feedback: "Feedback: {title}"
|
|
854
878
|
assert.strictEqual(sources[2].session.name, 'Feedback: {title}', 'my-prs-feedback should prefix with Feedback:');
|
|
879
|
+
|
|
880
|
+
// my-prs-attention: "{_attention_label}: {title}" (dynamic based on detected conditions)
|
|
881
|
+
assert.ok(sources[3].session.name.includes('_attention_label'), 'my-prs-attention should use dynamic label');
|
|
855
882
|
});
|
|
856
883
|
|
|
857
884
|
test('linear preset includes session name', async () => {
|