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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Version 0.1.0 is publicly released. Remote installation through GitHub CLI and l

- Added deterministic scoring based on freshness, repository activity, stars, discussion size, labels, reactions, and maintainer participation.
- Added recent claim detection for assignments and common claim phrases.
- Added a repeatable excluded-label search filter for removing blocked or noisy workflows.
- Added tests, cross-platform CI, security notes, and contributor documentation.
- Published the repository and v0.1.0 release with GitHub CLI extension discovery metadata.

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Focus the search:
```bash
gh issue-scout parser --language Python --min-stars 500
gh issue-scout --org home-assistant --label "help wanted"
gh issue-scout --label "good first issue" --exclude-label blocked
gh issue-scout --repo rust-lang/rust --label E-easy --max-age 30
```

Expand All @@ -48,6 +49,7 @@ Useful controls:

```text
--claim-window DAYS Treat recent claim comments as active for this long
--exclude-label LABEL Exclude a label; repeat for multiple labels
--include-claimed Show claimed candidates and their evidence
--include-assigned Include assigned issues in the GitHub search
--max-repo-idle DAYS Exclude repositories without a recent push
Expand Down
6 changes: 6 additions & 0 deletions issue_scout/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ def parser() -> argparse.ArgumentParser:
)
command.add_argument("keywords", nargs="*", help="optional search terms")
command.add_argument("--label", action="append", help="label to require; repeat for multiple labels")
command.add_argument(
"--exclude-label",
action="append",
help="label to exclude; repeat for multiple labels",
)
command.add_argument("--language", help="repository primary language")
command.add_argument("--repo", help="limit to OWNER/REPOSITORY")
command.add_argument("--org", help="limit to one organization")
Expand Down Expand Up @@ -67,6 +72,7 @@ def main(argv: list[str] | None = None) -> int:
search_query = build_query(
keywords=arguments.keywords,
labels=arguments.label,
excluded_labels=arguments.exclude_label,
language=arguments.language,
repository=arguments.repo,
organization=arguments.org,
Expand Down
3 changes: 3 additions & 0 deletions issue_scout/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def build_query(
*,
keywords: list[str] | None = None,
labels: list[str] | None = None,
excluded_labels: list[str] | None = None,
language: str | None = None,
repository: str | None = None,
organization: str | None = None,
Expand All @@ -38,6 +39,8 @@ def build_query(
parts.append("no:assignee")
for label in labels or ["good first issue"]:
parts.append(f"label:{quote_value(label)}")
for label in excluded_labels or []:
parts.append(f"-label:{quote_value(label)}")
if language:
parts.append(f"language:{quote_value(language)}")
if repository:
Expand Down
9 changes: 9 additions & 0 deletions tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,14 @@ def test_repo_and_org_are_mutually_exclusive(self) -> None:
with self.assertRaisesRegex(ValueError, "cannot be used together"):
build_query(repository="owner/repo", organization="owner")

def test_excluded_labels_are_repeatable_and_safely_quoted(self) -> None:
query = build_query(
labels=["help wanted"],
excluded_labels=["blocked", "needs info"],
now=datetime(2026, 8, 29, tzinfo=timezone.utc),
)

self.assertIn('label:"help wanted" -label:blocked -label:"needs info"', query)

def test_quote_value_removes_embedded_quotes(self) -> None:
self.assertEqual(quote_value('good "first" issue'), '"good first issue"')