diff --git a/AGENTS.md b/AGENTS.md index b6e98e2..6bfe6d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,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 an optional issue-activity cutoff through `--updated-within`. - Added tests, cross-platform CI, security notes, and contributor documentation. - Published the repository and v0.1.0 release with GitHub CLI extension discovery metadata. diff --git a/README.md b/README.md index 3ab1831..f7d243a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Focus the search: 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 +gh issue-scout --repo rust-lang/rust --label E-easy --max-age 30 --updated-within 7 ``` Generate output for another tool: @@ -53,6 +53,7 @@ Useful controls: --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 +--updated-within DAYS Require recent activity on the issue itself --query QUALIFIERS Append raw GitHub search qualifiers --show-query Print the exact generated search query --open Open the top result in a browser diff --git a/issue_scout/cli.py b/issue_scout/cli.py index d81a7e7..27a36fb 100644 --- a/issue_scout/cli.py +++ b/issue_scout/cli.py @@ -33,6 +33,11 @@ def parser() -> argparse.ArgumentParser: command.add_argument("--query", help="append raw GitHub search qualifiers") command.add_argument("--min-stars", type=int, default=50, help="minimum repository stars (default: 50)") command.add_argument("--max-age", type=int, default=90, help="maximum issue age in days (default: 90)") + command.add_argument( + "--updated-within", + type=int, + help="require issue activity within this many days", + ) command.add_argument( "--max-repo-idle", type=int, @@ -77,6 +82,7 @@ def main(argv: list[str] | None = None) -> int: repository=arguments.repo, organization=arguments.org, max_age_days=arguments.max_age, + updated_within_days=arguments.updated_within, include_assigned=arguments.include_assigned, extra_query=arguments.query, ) diff --git a/issue_scout/query.py b/issue_scout/query.py index 7e477d3..3d8a1f7 100644 --- a/issue_scout/query.py +++ b/issue_scout/query.py @@ -22,6 +22,7 @@ def build_query( repository: str | None = None, organization: str | None = None, max_age_days: int = 90, + updated_within_days: int | None = None, include_assigned: bool = False, extra_query: str | None = None, now: datetime | None = None, @@ -31,6 +32,8 @@ def build_query( raise ValueError("--repo and --org cannot be used together") if max_age_days < 1: raise ValueError("--max-age must be at least 1 day") + if updated_within_days is not None and updated_within_days < 1: + raise ValueError("--updated-within must be at least 1 day") current = now or datetime.now(timezone.utc) created_after = (current - timedelta(days=max_age_days)).date().isoformat() @@ -48,6 +51,9 @@ def build_query( if organization: parts.append(f"org:{quote_value(organization)}") parts.append(f"created:>={created_after}") + if updated_within_days is not None: + updated_after = (current - timedelta(days=updated_within_days)).date().isoformat() + parts.append(f"updated:>={updated_after}") if keywords: parts.extend(keyword.strip() for keyword in keywords if keyword.strip()) if extra_query: diff --git a/tests/test_query.py b/tests/test_query.py index 81b76c8..d476cc1 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -45,5 +45,17 @@ def test_excluded_labels_are_repeatable_and_safely_quoted(self) -> None: self.assertIn('label:"help wanted" -label:blocked -label:"needs info"', query) + def test_updated_within_adds_activity_cutoff(self) -> None: + query = build_query( + updated_within_days=7, + now=datetime(2026, 8, 29, tzinfo=timezone.utc), + ) + + self.assertIn("created:>=2026-05-31 updated:>=2026-08-22", query) + + def test_updated_within_rejects_non_positive_days(self) -> None: + with self.assertRaisesRegex(ValueError, "--updated-within must be at least 1 day"): + build_query(updated_within_days=0) + def test_quote_value_removes_embedded_quotes(self) -> None: self.assertEqual(quote_value('good "first" issue'), '"good first issue"')