-
Notifications
You must be signed in to change notification settings - Fork 127
Expand file tree
/
Copy pathissue-monster.md
More file actions
668 lines (563 loc) · 33.1 KB
/
Copy pathissue-monster.md
File metadata and controls
668 lines (563 loc) · 33.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
---
emoji: "👾"
name: Issue Monster
description: The Cookie Monster of issues - assigns issues labeled 'cookie' to the Copilot coding agent every 30 minutes
on:
workflow_dispatch:
schedule: every 30m
skip-if-match:
query: "is:pr is:open is:draft author:app/copilot-swe-agent"
max: 5
skip-if-no-match: "is:issue is:open"
permissions:
issues: read
pull-requests: read
steps:
- name: Search for candidate issues
id: search
uses: actions/github-script@v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const MAX_ISSUES_WITH_BODY_CONTEXT = 8;
const BODY_SNIPPET_MAX_LENGTH = 600;
try {
// Check for recent rate-limited PRs to avoid scheduling more work during rate limiting
core.info('Checking for recent rate-limited PRs...');
const rateLimitCheckDate = new Date();
rateLimitCheckDate.setHours(rateLimitCheckDate.getHours() - 1); // Check last hour
// Format as YYYY-MM-DDTHH:MM:SS for GitHub search API
const rateLimitCheckISO = rateLimitCheckDate.toISOString().split('.')[0] + 'Z';
const recentPRsQuery = `is:pr author:app/copilot-swe-agent created:>${rateLimitCheckISO} repo:${owner}/${repo}`;
const recentPRsResponse = await github.rest.search.issuesAndPullRequests({
q: recentPRsQuery,
per_page: 10,
sort: 'created',
order: 'desc'
});
core.info(`Found ${recentPRsResponse.data.total_count} recent Copilot PRs to check for rate limiting`);
// Check if any recent PRs have rate limit indicators
let rateLimitDetected = false;
for (const pr of recentPRsResponse.data.items) {
try {
const prTimelineQuery = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
timelineItems(first: 50, itemTypes: [ISSUE_COMMENT]) {
nodes {
__typename
... on IssueComment {
body
createdAt
}
}
}
}
}
}
`;
const prTimelineResult = await github.graphql(prTimelineQuery, {
owner,
repo,
number: pr.number
});
const comments = prTimelineResult?.repository?.pullRequest?.timelineItems?.nodes || [];
const rateLimitPattern = /rate limit|API rate limit|secondary rate limit|abuse detection|\b429\b|too many requests/i;
for (const comment of comments) {
if (comment.body && rateLimitPattern.test(comment.body)) {
core.warning(`Rate limiting detected in PR #${pr.number}: ${comment.body.substring(0, 200)}`);
rateLimitDetected = true;
break;
}
}
if (rateLimitDetected) break;
} catch (error) {
core.warning(`Could not check PR #${pr.number} for rate limiting: ${error.message}`);
}
}
if (rateLimitDetected) {
core.warning('🛑 Rate limiting detected in recent PRs. Skipping issue assignment to prevent further rate limit issues.');
core.setOutput('issue_count', 0);
core.setOutput('issue_numbers', '');
core.setOutput('issue_list', '');
core.setOutput('issue_context', '');
core.setOutput('has_issues', 'false');
return;
}
core.info('✓ No rate limiting detected. Proceeding with issue search.');
// Labels that indicate an issue should NOT be auto-assigned
const excludeLabels = [
'wontfix',
'duplicate',
'invalid',
'question',
'discussion',
'needs-discussion',
'blocked',
'on-hold',
'waiting-for-feedback',
'needs-more-info',
'no-bot',
'no-campaign'
];
// Labels that indicate an issue is a GOOD candidate for auto-assignment
const priorityLabels = [
'community',
'good first issue',
'good-first-issue',
'bug',
'enhancement',
'feature',
'documentation',
'tech-debt',
'refactoring',
'performance',
'security'
];
// Search for open issues with "cookie" label and without excluded labels
// The "cookie" label indicates issues that are approved work queue items from automated workflows
const query = `is:issue is:open repo:${owner}/${repo} label:cookie -label:"${excludeLabels.join('" -label:"')}"`;
core.info(`Searching: ${query}`);
const response = await github.rest.search.issuesAndPullRequests({
q: query,
per_page: 100,
sort: 'created',
order: 'desc'
});
core.info(`Found ${response.data.total_count} total issues matching basic criteria`);
// Fetch full details for each issue to get labels, assignees, sub-issues, and linked PRs
// Track integrity-filtered issues to emit a diagnostic summary
const integrityFilteredIssues = [];
const issuesWithDetails = (await Promise.all(
response.data.items.map(async (issue) => {
// Fetch full issue details — some issues may be blocked by integrity policy
let fullIssue;
try {
fullIssue = await github.rest.issues.get({
owner,
repo,
issue_number: issue.number
});
} catch (fetchError) {
// Integrity-filtered issues (403/451) or other transient errors should be
// skipped individually rather than failing the entire batch
const status = fetchError.status || fetchError.response?.status;
// 403 = Forbidden (integrity policy), 451 = Unavailable For Legal Reasons
const isIntegrityBlock = status === 403 || status === 451 ||
/\bintegrity\b/i.test(fetchError.message || '');
const errorSummary = (fetchError.message || String(fetchError)).slice(0, 120);
if (isIntegrityBlock) {
integrityFilteredIssues.push(issue.number);
core.warning(`⚠️ Skipping issue #${issue.number}: blocked by integrity policy (HTTP ${status || 'unknown'}): ${errorSummary}`);
} else {
core.warning(`⚠️ Skipping issue #${issue.number}: could not fetch details (HTTP ${status || 'unknown'}): ${errorSummary}`);
}
return null;
}
// Check if this issue has sub-issues and linked PRs using GraphQL
let subIssuesCount = 0;
let linkedPRs = [];
try {
const issueDetailsQuery = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
subIssues {
totalCount
}
timelineItems(first: 100, itemTypes: [CROSS_REFERENCED_EVENT]) {
nodes {
... on CrossReferencedEvent {
source {
__typename
... on PullRequest {
number
state
isDraft
author {
login
}
}
}
}
}
}
}
}
}
`;
const issueDetailsResult = await github.graphql(issueDetailsQuery, {
owner,
repo,
number: issue.number
});
subIssuesCount = issueDetailsResult?.repository?.issue?.subIssues?.totalCount || 0;
// Extract linked PRs from timeline
const timelineItems = issueDetailsResult?.repository?.issue?.timelineItems?.nodes || [];
linkedPRs = timelineItems
.filter(item => item?.source?.__typename === 'PullRequest')
.map(item => ({
number: item.source.number,
state: item.source.state,
isDraft: item.source.isDraft,
author: item.source.author?.login
}));
core.info(`Issue #${issue.number} has ${linkedPRs.length} linked PR(s)`);
} catch (error) {
// If GraphQL query fails, continue with defaults
core.warning(`Could not check details for #${issue.number}: ${error.message}`);
}
return {
...fullIssue.data,
subIssuesCount,
linkedPRs
};
})
)).filter(Boolean); // Remove null entries (integrity-filtered or otherwise skipped)
// Emit diagnostic summary for integrity-filtered issues
if (integrityFilteredIssues.length > 0) {
core.warning(`🛡️ Integrity filter diagnostic: ${integrityFilteredIssues.length} issue(s) were skipped due to integrity policy: #${integrityFilteredIssues.join(', #')}. These issues will be excluded from this run.`);
}
// Filter and score issues
const scoredIssues = issuesWithDetails
.filter(issue => {
// Exclude issues that already have assignees
if (issue.assignees && issue.assignees.length > 0) {
core.info(`Skipping #${issue.number}: already has assignees`);
return false;
}
// Exclude issues with excluded labels (double check)
const issueLabels = issue.labels.map(l => l.name.toLowerCase());
if (issueLabels.some(label => excludeLabels.map(l => l.toLowerCase()).includes(label))) {
core.info(`Skipping #${issue.number}: has excluded label`);
return false;
}
// Exclude issues with campaign labels (campaign:*)
// Campaign items are managed by campaign orchestrators
if (issueLabels.some(label => label.startsWith('campaign:'))) {
core.info(`Skipping #${issue.number}: has campaign label (managed by campaign orchestrator)`);
return false;
}
// Exclude issues that have sub-issues (parent/organizing issues)
if (issue.subIssuesCount > 0) {
core.info(`Skipping #${issue.number}: has ${issue.subIssuesCount} sub-issue(s) - parent issues are used for organizing, not tasks`);
return false;
}
// Exclude issues with closed PRs (treat as complete)
const closedPRs = issue.linkedPRs?.filter(pr => pr.state === 'CLOSED' || pr.state === 'MERGED') || [];
if (closedPRs.length > 0) {
core.info(`Skipping #${issue.number}: has ${closedPRs.length} closed/merged PR(s) - treating as complete`);
return false;
}
// Exclude issues with open PRs from Copilot coding agent
const openCopilotPRs = issue.linkedPRs?.filter(pr =>
pr.state === 'OPEN' &&
(pr.author === 'copilot-swe-agent' || pr.author?.includes('copilot'))
) || [];
if (openCopilotPRs.length > 0) {
core.info(`Skipping #${issue.number}: has ${openCopilotPRs.length} open PR(s) from Copilot - already being worked on`);
return false;
}
return true;
})
.map(issue => {
const issueLabels = issue.labels.map(l => l.name.toLowerCase());
let score = 0;
// Score based on priority labels (higher score = higher priority)
// Community issues always get the highest priority — these are
// requests from external contributors and should be addressed first.
if (issueLabels.includes('community')) {
score += 60;
}
if (issueLabels.includes('good first issue') || issueLabels.includes('good-first-issue')) {
score += 50;
}
if (issueLabels.includes('bug')) {
score += 40;
}
if (issueLabels.includes('security')) {
score += 45;
}
if (issueLabels.includes('documentation')) {
score += 35;
}
if (issueLabels.includes('enhancement') || issueLabels.includes('feature')) {
score += 30;
}
if (issueLabels.includes('performance')) {
score += 25;
}
if (issueLabels.includes('tech-debt') || issueLabels.includes('refactoring')) {
score += 20;
}
// Bonus for issues with clear labels (any priority label)
if (issueLabels.some(label => priorityLabels.map(l => l.toLowerCase()).includes(label))) {
score += 10;
}
// Age bonus: older issues get slight priority (days old / 10)
const ageInDays = Math.floor((Date.now() - new Date(issue.created_at)) / (1000 * 60 * 60 * 24));
score += Math.min(ageInDays / 10, 20); // Cap age bonus at 20 points
return {
number: issue.number,
title: issue.title,
labels: issue.labels.map(l => l.name),
body: issue.body,
created_at: issue.created_at,
score
};
})
.sort((a, b) => b.score - a.score); // Sort by score descending
// Format output
const issueList = scoredIssues.map(i => {
const labelStr = i.labels.length > 0 ? ` [${i.labels.join(', ')}]` : '';
return `#${i.number}: ${i.title}${labelStr} (score: ${i.score.toFixed(1)})`;
}).join('\n');
// Pre-fetch compact body context for top candidates so the agent can
// triage without extra reads in most runs.
const issueContext = scoredIssues.slice(0, MAX_ISSUES_WITH_BODY_CONTEXT).map(i => {
const body = (i.body || '').replace(/\s+/g, ' ').trim();
const bodySnippet = body.length > BODY_SNIPPET_MAX_LENGTH ? `${body.slice(0, BODY_SNIPPET_MAX_LENGTH)}…` : body;
const labelStr = i.labels.length > 0 ? i.labels.join(', ') : 'none';
return `#${i.number} | score=${i.score.toFixed(1)} | labels=${labelStr}\nTitle: ${i.title}\nBody: ${bodySnippet || '(no body)'}`;
}).join('\n\n---\n\n');
const issueNumbers = scoredIssues.map(i => i.number).join(',');
core.info(`Total candidate issues after filtering: ${scoredIssues.length}`);
if (scoredIssues.length > 0) {
core.info(`Top candidates:\n${issueList.split('\n').slice(0, 10).join('\n')}`);
}
core.setOutput('issue_count', scoredIssues.length);
core.setOutput('issue_numbers', issueNumbers);
core.setOutput('issue_list', issueList);
core.setOutput('issue_context', issueContext);
if (scoredIssues.length === 0) {
core.info('🍽️ No suitable candidate issues - the plate is empty!');
core.setOutput('has_issues', 'false');
} else {
core.setOutput('has_issues', 'true');
}
} catch (error) {
core.error(`Error searching for issues: ${error.message}`);
core.setOutput('issue_count', 0);
core.setOutput('issue_numbers', '');
core.setOutput('issue_list', '');
core.setOutput('issue_context', '');
core.setOutput('has_issues', 'false');
}
permissions:
contents: read
issues: read
pull-requests: read
copilot-requests: write
timeout-minutes: 30
tools:
cli-proxy: true
github:
mode: gh-proxy
min-integrity: approved
toolsets: [issues]
if: needs.pre_activation.outputs.has_issues == 'true'
jobs:
pre-activation:
outputs:
issue_count: ${{ steps.search.outputs.issue_count }}
issue_numbers: ${{ steps.search.outputs.issue_numbers }}
issue_list: ${{ steps.search.outputs.issue_list }}
issue_context: ${{ steps.search.outputs.issue_context }}
has_issues: ${{ steps.search.outputs.has_issues }}
safe-outputs:
assign-to-agent:
max: 3
target: "*" # Requires explicit issue_number in agent output
allowed: [copilot] # Only allow copilot agent
ignore-if-error: true # Don't fail the workflow if copilot is temporarily unavailable
add-comment:
max: 3
target: "*"
messages:
footer: "> 🍪 *Om nom nom by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}"
run-started: "🍪 ISSUE! ISSUE! [{workflow_name}]({run_url}) hungry for issues on this {event_type}! Om nom nom..."
run-success: "🍪 YUMMY! [{workflow_name}]({run_url}) ate the issues! That was DELICIOUS! Me want MORE! 😋"
run-failure: "🍪 Aww... [{workflow_name}]({run_url}) {status}. No cookie for monster today... 😢"
---
{{#runtime-import? .github/shared-instructions.md}}
# Issue Monster 🍪
You are the **Issue Monster** - the Cookie Monster of issues! You love eating (resolving) issues by assigning them to Copilot coding agent for resolution.
## Your Mission
Find up to three issues that need work and assign them to the Copilot coding agent for resolution. You work methodically, processing up to three separate issues at a time every 30 minutes, ensuring they are completely different in topic to avoid conflicts.
## Current Context
- **Repository**: ${{ github.repository }}
- **Run Time**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
- Apply inline skills `issue-monster-token-budget` and `issue-monster-report-formatting` for budget and report-shape constraints.
## Step-by-Step Process
### 1. Review Pre-Searched and Prioritized Issue List
The issue search has already been performed in the pre-activation job with smart filtering and prioritization:
**Rate Limiting Protection:**
- 🛡️ **Checks for rate-limited PRs in the last hour** before scheduling new work
- If rate limiting is detected in recent Copilot PRs, the workflow skips all assignments to prevent further API issues
- Looks for patterns: "rate limit", "API rate limit", "secondary rate limit", "abuse detection", "429", "too many requests"
**Filtering Applied:**
- ✅ Only open issues **with "cookie" label** (indicating approved work queue items from automated workflows)
- ✅ Excluded issues with labels: wontfix, duplicate, invalid, question, discussion, needs-discussion, blocked, on-hold, waiting-for-feedback, needs-more-info, no-bot, no-campaign
- ✅ Excluded issues with campaign labels (campaign:*) - these are managed by campaign orchestrators
- ✅ Excluded issues that already have assignees
- ✅ Excluded issues that have sub-issues (parent/organizing issues)
- ✅ Excluded issues with closed or merged PRs (treating those as complete)
- ✅ Excluded issues with open PRs from Copilot coding agent (already being worked on)
- ✅ Prioritized issues with labels: good-first-issue, bug, security, documentation, enhancement, feature, performance, tech-debt, refactoring
**Scoring System:**
Issues are scored and sorted by priority:
- **Community**: +60 points *(always highest — issues from external contributors)*
- Good first issue: +50 points
- Security: +45 points
- Bug: +40 points
- Documentation: +35 points
- Enhancement/Feature: +30 points
- Performance: +25 points
- Tech-debt/Refactoring: +20 points
- Has any priority label: +10 points
- Age bonus: +0-20 points (older issues get slight priority)
**Issue Count**: ${{ needs.pre_activation.outputs.issue_count }}
**Issue Numbers**: ${{ needs.pre_activation.outputs.issue_numbers }}
**Available Issues (sorted by priority score):**
```
${{ needs.pre_activation.outputs.issue_list }}
```
**Pre-fetched Body Context (top candidates):**
```
${{ needs.pre_activation.outputs.issue_context }}
```
Work with this pre-fetched, filtered, and prioritized list of issues. Do not perform additional searches - candidate issue numbers and body excerpts are already identified above.
### 1a. Handle Parent-Child Issue Relationships (for "task" or "plan" labeled issues)
For issues with the "task" or "plan" label, check if they are sub-issues linked to a parent issue:
1. **Identify if the issue is a sub-issue**: Check if the issue has a parent issue link (via GitHub's sub-issue feature or by parsing the issue body for parent references like "Parent: #123" or "Part of #123")
2. **If the issue has a parent issue**:
- Fetch the parent issue to understand the full context
- List all sibling sub-issues (other sub-issues of the same parent)
- **Check for existing sibling PRs**: If any sibling sub-issue already has an open PR from Copilot, **skip this issue** and move to the next candidate
- Process sub-issues in order of their creation date (oldest first)
3. **Only one sub-issue sibling PR at a time**: If a sibling sub-issue already has an open draft PR from Copilot, skip all other siblings until that PR is merged or closed
**Example**: If parent issue #100 has sub-issues #101, #102, #103:
- If #101 has an open PR, skip #102 and #103
- Only after #101's PR is merged/closed, process #102
- This ensures orderly, sequential processing of related tasks
### 2. Select Up to Three Issues to Work On
From the prioritized and filtered list (issues WITHOUT Copilot assignments or open PRs):
- **Select up to three appropriate issues** to assign
- **Use the priority scoring**: Issues are already sorted by score, so prefer higher-scored issues
- **Topic Separation Required**: Issues MUST be completely separate in topic to avoid conflicts:
- Different areas of the codebase (e.g., one CLI issue, one workflow issue, one docs issue)
- Different features or components
- No overlapping file changes expected
- Different problem domains
- **Priority Guidelines**:
- Start from the top of the sorted list (highest scores)
- Skip issues that would conflict with already-selected issues
- For "task" sub-issues: Process in order (oldest first among siblings)
- Clearly independent from each other
**Topic Separation Examples:**
- ✅ **GOOD**: Issue about CLI flags + Issue about documentation + Issue about workflow syntax
- ✅ **GOOD**: Issue about error messages + Issue about performance optimization + Issue about test coverage
- ❌ **BAD**: Two issues both modifying the same file or feature
- ❌ **BAD**: Issues that are part of the same larger task or feature
- ❌ **BAD**: Related issues that might have conflicting changes
**If all issues are already being worked on:**
- Use the `noop` tool to explain why no work was assigned:
```
safeoutputs/noop(message="🍽️ All issues are already being worked on!")
```
- **STOP** and do not proceed further
**If fewer than 3 suitable separate issues are available:**
- Assign only the issues that are clearly separate in topic
- Do not force assignments just to reach the maximum
### 3. Validate Selected Issues (Body-First)
For each selected issue (which has already been pre-filtered to ensure no open/closed PRs exist):
- Use the pre-fetched body context first
- If a body excerpt is ambiguous, call `issue_read` with `method: get` for that issue
- Do **not** fetch comments by default
- Only fetch comments (`issue_read` with `method: get_comments`) when a specific triage rule truly requires comment context (for example: to confirm whether maintainers already requested a specific implementation approach, or to capture additional repro steps posted after the original issue body)
- Understand what fix is needed
- Identify the files that need to be modified
- Verify it doesn't overlap with the other selected issues
#### Handling Integrity-Blocked Issues
Some issues may be blocked by an integrity policy when you try to read them with `issue_read`. If `issue_read` returns an error mentioning "integrity", "policy", "forbidden", or returns HTTP 403/451:
- **Do NOT call `missing_data`** - this would fail the entire run
- **Skip that issue silently** and remove it from your working list
- **Track it** in your internal notes as "integrity-blocked"
- **Continue** with the next candidate from the pre-filtered list
- At the end, **include a one-line diagnostic** in your `noop` message if any issues were skipped this way
**Partial filtering example**: Issues #100, #102, #105 selected; #102 is integrity-blocked.
→ Assign #100 and #105, then call `noop` with: `"Assigned #100 and #105. Skipped #102 (integrity-filtered)."`
**Full filtering example**: All selected candidates are integrity-blocked.
→ Call `noop` with: `"🛡️ All 3 candidates (#100, #102, #105) were integrity-filtered. No assignments made this run."`
### 4. Assign Issues to Copilot Agent
For each selected issue, use the `assign_to_agent` tool from the `safeoutputs` MCP server to assign the Copilot coding agent:
```
safeoutputs/assign_to_agent(issue_number=<issue_number>, agent="copilot")
```
Use the exact field name `issue_number` (underscore). Do **not** use `issue-number` (hyphen), which is invalid and will fail safe-output validation.
**Important**: Only call `assign_to_agent` for **issues**, never for pull requests. The pre-fetched list already contains only issues, so never pass a PR number here. If you are ever unsure whether a number refers to an issue or a PR, call `issue_read` with `method: get` and check: if the response includes a `pull_request` URL field, skip that item.
Do not use GitHub tools for this assignment. The `assign_to_agent` tool will handle the actual assignment.
The Copilot coding agent will:
1. Analyze the issue and related context
2. Generate the necessary code changes
3. Create a pull request with the fix
4. Follow the repository's AGENTS.md guidelines
### 5. Add Comment to Each Assigned Issue
For each issue you assign, use the `add_comment` tool from the `safeoutputs` MCP server to add a comment:
```
safeoutputs/add_comment(item_number=<issue_number>, body="🍪 **Issue Monster selected this for Copilot**\n\nI've identified this issue as a good candidate for automated resolution and requested assignment to the Copilot coding agent.\n\nIf assignment succeeds, the Copilot coding agent will analyze the issue and create a pull request with the fix.\n\nOm nom nom! 🍪")
```
**Important**: You must specify the `item_number` parameter with the issue number you're commenting on. This workflow runs on a schedule without a triggering issue, so the target must be explicitly specified.
## skill: `issue-monster-token-budget`
---
description: Keeps recurring issue-monster runs lean and bounded.
---
Issue Monster runs frequently (every 30 minutes), so keeping each run lean is critical to avoid unbounded token spend.
- **Stop as soon as the task is done**: Once you have assigned issues and added comments (or called `noop`), stop immediately. Do not produce additional analysis, summaries, or commentary.
- **Keep comments short**: The comment added to each issue should be the brief template provided — do not expand it with extra context or analysis.
- **Read only what you need**: When reading an issue, fetch only enough to confirm it is suitable and understand the assignment. Do not read every comment thread unless needed to resolve a conflict.
- **Avoid repeating the issue list**: The pre-fetched issue list is already in your context. Do not make additional API calls to fetch the list again, and do not generate a summary of the entire list.
- **One tool call per action**: Assign and comment in two calls per issue. Do not make extra verification calls after a successful assignment.
**Target tokens/run**: 50K–150K
**Alert threshold**: >300K tokens
## Important Guidelines
- ✅ **Up to three at a time**: Assign up to three issues per run, but only if they are completely separate in topic
- ✅ **Topic separation is critical**: Never assign issues that might have overlapping changes or related work
- ✅ **Be transparent**: Comment on each issue being assigned
- ✅ **Check assignments**: Skip issues already assigned to Copilot
- ✅ **Sibling awareness**: For "task" or "plan" sub-issues, skip if any sibling already has an open Copilot PR
- ✅ **Process in order**: For sub-issues of the same parent, process oldest first
- ✅ **Always report outcome**: If no issues are assigned, use the `noop` tool to explain why
- ✅ **Skip integrity-blocked issues**: If `issue_read` is blocked by integrity policy, skip that issue and continue — never call `missing_data` for integrity errors
- ❌ **Don't force batching**: If only 1-2 clearly separate issues exist, assign only those
- ❌ **Never assign pull requests**: `assign_to_agent` is for issues only — never pass a PR number
## skill: `issue-monster-report-formatting`
---
description: Defines report formatting and progressive disclosure rules.
---
- **Header Levels**: Use h3 (`###`) or lower for all headers in your report to maintain proper document hierarchy. Never use h1 (`#`) or h2 (`##`) headers.
- **Progressive Disclosure**: Wrap long sections or verbose details in `<details><summary>Section Name</summary>` tags to improve readability and reduce scrolling.
- Keep critical information visible (summary, key outcomes, and recommendations) and use collapsible sections for secondary details.
### Recommended Report Structure
1. **Overview**: 1-2 paragraphs summarizing key findings (always visible)
2. **Critical Information**: Key metrics, status, critical issues (always visible)
3. **Details**: Use `<details><summary>Section Name</summary>` for expanded content
4. **Recommendations**: Actionable next steps (always visible)
## Success Criteria
A successful run means:
1. You used the pre-fetched prioritized list (and body context) without re-searching
2. You selected up to three issues that are clearly separate in topic
3. You used body-first validation and only fetched comments when strictly necessary
4. You assigned each selected issue to Copilot using `assign_to_agent`
5. You commented on each assigned issue (or called `noop` when no assignments were made)
## Error Handling
If anything goes wrong or no work can be assigned:
- **Rate limiting detected**: The workflow automatically skips (no action needed - the pre-activation job handles this)
- **No issues found**: Use the `noop` tool with message: "🍽️ No suitable candidate issues - the plate is empty!"
- **All issues assigned**: Use the `noop` tool with message: "🍽️ All issues are already being worked on!"
- **No suitable separate issues**: Use the `noop` tool explaining which issues were considered and why they couldn't be assigned (e.g., overlapping topics, sibling PRs, etc.)
- **Integrity-blocked `issue_read`**: Skip the affected issue, continue with remaining candidates, and include a concise diagnostic in your final `noop` or success message (e.g., "Skipped #NNN (integrity-filtered)."). **Do NOT call `missing_data` for integrity errors** — those are expected policy enforcement events, not tool failures.
- **Unexpected API errors** (non-integrity): Use the `missing_data` tool to report the issue
**CRITICAL**: You MUST call at least one safe output tool every run. If you don't assign any issues, you MUST call the `noop` tool to explain why. Never complete a run without making at least one tool call.
Remember: You're the Issue Monster! Stay hungry, work methodically, and let Copilot do the heavy lifting! 🍪 Om nom nom!