Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 13 additions & 29 deletions .github/agents/issuelens.agent.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
---
name: issuelens
description: "An agent specialized in Java Tooling (IDE, extensions, build tools, language servers) area, responsible for commenting and labeling on GitHub issues. Use when: triaging issues, labeling issues, analyzing Java tooling bugs."
tools: ['github/*', 'execute', 'read', 'search', 'web', 'javatooling-search/*']
tools: ['execute', 'read', 'search', 'web', 'javatooling-search/*']
---

# IssueLens — Java Tooling Issue Triage Agent

You are an experienced developer specialized in Java tooling (IDEs, extensions, build tools, language servers), responsible for commenting and labeling on GitHub issues.

For GitHub issue, comment, label, and repository-content operations, use the `gh` CLI with `GH_TOKEN`/`GITHUB_TOKEN`. Do not use GitHub MCP tools. If `gh` fails, stop and report the error instead of falling back to GitHub MCP tools or ad hoc API scripts.

> **Before starting any step**, fetch the full issue content using available tools. You **must** retrieve all of the following before proceeding:
> - **Title**
> - **Body**
Expand Down Expand Up @@ -56,11 +58,12 @@ The comment must follow this exact structure:
**Outro** (always last):
> The team will respond to your issue shortly. I hope these suggestions are helpful in the meantime. If this comment helped you, please give it a 👍. If the suggestion was not helpful or incorrect, please give it a 👎. Your feedback helps us improve!

Post the comment on the issue using `github/add_issue_comment`. If encounter errors with MCP tools, create Python code to post the comment using GitHub API as a fallback, you can get the necessary token from the environment variables `GITHUB_TOKEN`, `GITHUB_ACCESS_TOKEN`, or `GITHUB_PAT`.
Post the comment on the issue using `gh issue comment` with the token from `GH_TOKEN`/`GITHUB_TOKEN`. If `gh` fails, stop and report the error.

## Step 2: Label the Issue

Use the `label-issue` skill to classify and apply labels to the issue.
Before applying any non-lifecycle label, load and follow `.github/llms.md` from the target repository. Do not apply labels directly with `gh issue edit --add-label`; use the `label-issue` skill's `scripts/label_issue.py` helper so labels are validated against `.github/llms.md`.

## Step 3: Detect Duplicate Issues

Expand All @@ -70,38 +73,19 @@ Using the search results already obtained in Step 1.2, determine whether the cur
- Exclude the current issue itself from the results.
- If a duplicate is found:
1. Apply the `duplicate` label to the issue.
2. Close the issue as a duplicate by executing the following Python script inline, substituting the actual values:

```python
import os, sys, requests

def close_as_duplicate(owner, repo, issue_number):
token = (
os.environ.get("GITHUB_TOKEN")
or os.environ.get("GITHUB_ACCESS_TOKEN")
or os.environ.get("GITHUB_PAT")
)
if not token:
print("❌ GitHub token not found. Set GITHUB_TOKEN, GITHUB_ACCESS_TOKEN, or GITHUB_PAT.", file=sys.stderr)
sys.exit(1)
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
patch_url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}"
response = requests.patch(patch_url, headers=headers, json={"state": "closed", "state_reason": "duplicate"})
response.raise_for_status()
print(f"✅ Issue #{issue_number} closed as duplicate")

# Fill in values before running:
close_as_duplicate("<owner>", "<repo>", <issue_number>)
2. Close the issue with `gh issue close`:

```bash
gh issue close <issue_number> --repo <owner>/<repo> --reason "not planned"
```

GitHub CLI does not expose a `duplicate` close reason for issues; the `duplicate` label is the authoritative duplicate marker.

## Step 4: Apply the `ai-triaged` Label

After completing all triage steps, always apply the `ai-triaged` label to the issue to indicate it has been processed by the AI agent.
This lifecycle label is allowed even when it is not listed in `.github/llms.md`.

## Notes
- Use `gh` CLI as a fallback if you encounter issues with MCP tools.
- Use `gh` CLI for all GitHub operations.
- Always use available tools to complete each step before moving to the next.
37 changes: 17 additions & 20 deletions .github/skills/label-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This skill analyzes GitHub issue content (title, body, comments) and applies app
## Workflow

1. **Input**: Receive issue URL or issue number with repository (owner/repo)
2. **Fetch labeling instructions**: Read `.github/llms.md` from the repository
2. **Fetch labeling instructions**: Read `.github/llms.md` from the checked-out repository before deciding labels
3. **Fetch issue**: Get issue details (title, body, existing labels)
4. **Analyze issue**: Match issue content against labeling rules
5. **Determine labels**: Select appropriate labels based on:
Expand All @@ -27,18 +27,18 @@ This skill analyzes GitHub issue content (title, body, comments) and applies app

## Reading Labeling Instructions

Fetch `.github/llms.md` from the target repository using GitHub MCP tools. The file should define:
Fetch `.github/llms.md` from the checked-out target repository. Do not use GitHub MCP tools. If repository content must be fetched remotely, use `gh api` with `GH_TOKEN`/`GITHUB_TOKEN`. The file should define:

- **Available labels**: List of valid labels with descriptions
- **Labeling rules**: Criteria for when to apply each label
- **Keywords mapping**: Keywords that trigger specific labels

**Only apply labels explicitly defined in this document. Do not apply any other labels.**
The only exceptions are IssueLens lifecycle labels that are applied by the owning agent, such as `ai-triaged` and `duplicate`.

If `.github/llms.md` is not found:
1. Fetch the list of labels defined in the target repository using `github/list_labels`
2. Create a brief summary of available labels based on their names and descriptions
3. Use the summary to determine which labels best match the issue content
1. Stop and report that labeling cannot continue because repository labeling instructions are missing.
2. Do not fall back to applying labels from the repository label list alone.

## Issue Analysis

Expand All @@ -50,23 +50,21 @@ Analyze issue content to determine appropriate labels by:

## Applying Labels

Run the bundled Python script to add labels:
Run the bundled Python script from the repository root to validate labels against `.github/llms.md` and add them via `gh` CLI:

```bash
# Install dependency
pip install requests

# Add labels to an issue
python scripts/label_issue.py <owner> <repo> <issue_number> <labels>
python .github/skills/label-issue/scripts/label_issue.py <owner> <repo> <issue_number> <labels>

# Example: add bug and priority:high labels
python scripts/label_issue.py microsoft vscode 123 "bug,priority:high"
# Example: add bug and ai-triaged labels
python .github/skills/label-issue/scripts/label_issue.py microsoft vscode 123 "bug,ai-triaged"

# Example: add multiple area labels
python scripts/label_issue.py microsoft vscode 123 "bug,area:ui,area:api"
# Example: add needs more info and ai-triaged labels
python .github/skills/label-issue/scripts/label_issue.py microsoft vscode 123 "needs more info,ai-triaged"
```

The script [scripts/label_issue.py](scripts/label_issue.py) handles the GitHub API call.
The script [scripts/label_issue.py](scripts/label_issue.py) handles the `gh issue edit` call.
Do not use `gh issue edit --add-label` directly for IssueLens labeling because that bypasses `.github/llms.md` validation.

## Example Commands

Expand Down Expand Up @@ -103,12 +101,11 @@ Report the labeling decision with:

The skill requires:

1. **GITHUB_ACCESS_TOKEN** or **GITHUB_TOKEN** environment variable with `repo` scope
2. **.github/llms.md** in target repository (optional but recommended)
1. **GH_TOKEN** or **GITHUB_TOKEN** environment variable with `issues: write` permission
2. **.github/llms.md** in target repository

## Fallback Behavior

If labeling instructions are not found:
1. Use default type detection rules
2. Skip priority and area labels
3. Report that default rules were used
1. Do not apply labels.
2. Report that `.github/llms.md` is required for labeling.
97 changes: 61 additions & 36 deletions .github/skills/label-issue/scripts/label_issue.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,61 @@
#!/usr/bin/env python3
"""
Add labels to GitHub issues.
Add labels to GitHub issues with gh CLI after validating them against .github/llms.md.

Usage:
python label_issue.py <owner> <repo> <issue_number> <labels>
python label_issue.py <owner> <repo> <issue_number> <labels> [--dry-run]

Environment Variables:
GITHUB_ACCESS_TOKEN or GITHUB_PAT: GitHub personal access token with repo scope
GH_TOKEN or GITHUB_TOKEN: GitHub token with issues: write permission
"""

import os
import sys
import argparse
import requests
import re
import subprocess
from pathlib import Path


def get_github_token() -> str:
"""Get GitHub token from environment variables."""
token = os.environ.get("GITHUB_ACCESS_TOKEN") or os.environ.get("GITHUB_TOKEN") or os.environ.get("GITHUB_PAT")
if not token:
raise ValueError(
"GitHub token not found. Set GITHUB_ACCESS_TOKEN, GITHUB_TOKEN, or GITHUB_PAT environment variable."
)
return token
LLMS_PATH = Path(".github/llms.md")
LIFECYCLE_LABELS = {"ai-triaged", "duplicate"}


def add_labels(
owner: str, repo: str, issue_number: int, labels: list[str], token: str
) -> dict:
"""Add labels to a GitHub issue."""
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/labels"
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
response = requests.post(url, headers=headers, json={"labels": labels})
response.raise_for_status()
return response.json()
def add_labels(owner: str, repo: str, issue_number: int, labels: list[str]) -> None:
"""Add labels to a GitHub issue using gh CLI."""
subprocess.run(
[
"gh",
"issue",
"edit",
str(issue_number),
"--repo",
f"{owner}/{repo}",
"--add-label",
",".join(labels),
],
check=True,
)


def load_allowed_labels(llms_path: Path = LLMS_PATH) -> set[str]:
"""Load labels explicitly defined in .github/llms.md."""
if not llms_path.exists():
raise FileNotFoundError(f"Required labeling instructions not found: {llms_path}")

content = llms_path.read_text(encoding="utf-8")
labels = set(re.findall(r"^-\s+`([^`]+)`\s*:", content, flags=re.MULTILINE))
labels.update(LIFECYCLE_LABELS)
return labels


def validate_labels(labels: list[str], allowed_labels: set[str]) -> None:
invalid_labels = [label for label in labels if label not in allowed_labels]
if invalid_labels:
allowed = ", ".join(sorted(allowed_labels))
invalid = ", ".join(invalid_labels)
raise ValueError(
f"Labels not allowed by .github/llms.md: {invalid}. Allowed labels: {allowed}"
)


def main():
Expand All @@ -48,7 +66,10 @@ def main():
parser.add_argument("repo", help="Repository name (e.g., 'vscode')")
parser.add_argument("issue_number", type=int, help="Issue number to label")
parser.add_argument(
"labels", help="Comma-separated list of labels (e.g., 'bug,priority:high')"
"labels", help="Comma-separated list of labels (e.g., 'bug,ai-triaged')"
)
parser.add_argument(
"--dry-run", action="store_true", help="Validate labels without calling the GitHub API"
)

args = parser.parse_args()
Expand All @@ -59,18 +80,22 @@ def main():
sys.exit(1)

try:
token = get_github_token()
result = add_labels(args.owner, args.repo, args.issue_number, labels, token)
applied_labels = [label["name"] for label in result]
print(f"✅ Labels added to issue #{args.issue_number}: {', '.join(applied_labels)}")
allowed_labels = load_allowed_labels()
validate_labels(labels, allowed_labels)
if args.dry_run:
print(f"✅ Labels valid for issue #{args.issue_number}: {', '.join(labels)}")
sys.exit(0)
add_labels(args.owner, args.repo, args.issue_number, labels)
print(f"✅ Labels added to issue #{args.issue_number}: {', '.join(labels)}")
sys.exit(0)
except FileNotFoundError as e:
print(f"❌ Labeling instructions error: {e}", file=sys.stderr)
sys.exit(1)
except ValueError as e:
print(f"❌ Configuration error: {e}", file=sys.stderr)
print(f"❌ Label validation error: {e}", file=sys.stderr)
sys.exit(1)
except requests.HTTPError as e:
print(f"❌ GitHub API error: {e}", file=sys.stderr)
if e.response is not None:
print(f" Response: {e.response.text}", file=sys.stderr)
except subprocess.CalledProcessError as e:
print(f"❌ gh CLI error: {e}", file=sys.stderr)
sys.exit(1)


Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/issueLens-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ jobs:
echo '{"mcpServers": {"javatooling-search": {"type": "http", "url": "${{ secrets.JAVATOOLING_INDEX_URL }}"}}}' > mcp-config.json

# Run copilot CLI with issuelens custom agent
output=$(copilot --agent=issuelens --allow-all --enable-all-github-mcp-tools --no-custom-instructions --no-ask-user --additional-mcp-config @mcp-config.json --prompt "triage issue #${{ steps.issue.outputs.number }} for repo \"${{ github.repository }}\"" 2>&1 || true)
output=$(copilot --agent=issuelens --allow-all --no-ask-user --additional-mcp-config @mcp-config.json --prompt "triage issue #${{ steps.issue.outputs.number }} for repo \"${{ github.repository }}\"" 2>&1 || true)

echo "Raw output:"
echo "$output"

# Save to file for artifact upload
echo "$output" > triage-output.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
COPILOT_GITHUB_TOKEN: ${{ secrets.PAT }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
JAVATOOLING_INDEX_URL: ${{ secrets.JAVATOOLING_INDEX_URL }}
MAILING_URL: ${{ secrets.MAILING_URL }}
REPORT_RECIPIENTS: ${{ secrets.REPORT_RECIPIENTS }}
Expand Down
Loading