diff --git a/plugins/hookify/core/config_loader.py b/plugins/hookify/core/config_loader.py index fa2fc3e36f..9311e52521 100644 --- a/plugins/hookify/core/config_loader.py +++ b/plugins/hookify/core/config_loader.py @@ -199,7 +199,10 @@ def load_rules(event: Optional[str] = None) -> List[Rule]: """Load all hookify rules from .claude directory. Args: - event: Optional event filter ("bash", "file", "stop", etc.) + event: Optional event filter ("bash", "file", "stop", "all", etc.) + If None, load all enabled rules. + If "all", load only rules tagged with event="all" (universal rules for unknown tools). + Otherwise, load rules matching the event OR tagged with event="all". Returns: List of enabled Rule objects matching the event. diff --git a/plugins/hookify/core/rule_engine.py b/plugins/hookify/core/rule_engine.py index 8244c00591..43afe01c4c 100644 --- a/plugins/hookify/core/rule_engine.py +++ b/plugins/hookify/core/rule_engine.py @@ -69,7 +69,8 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ "reason": combined_message, "systemMessage": combined_message } - elif hook_event in ['PreToolUse', 'PostToolUse']: + elif hook_event == 'PreToolUse': + # PreToolUse can deny the tool before it executes return { "hookSpecificOutput": { "hookEventName": hook_event, @@ -78,7 +79,7 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ "systemMessage": combined_message } else: - # For other events, just show message + # PostToolUse and other events: tool already ran, can only inject a message return { "systemMessage": combined_message } diff --git a/plugins/hookify/hooks/posttooluse.py b/plugins/hookify/hooks/posttooluse.py index a9e12cc797..564f45b3c1 100755 --- a/plugins/hookify/hooks/posttooluse.py +++ b/plugins/hookify/hooks/posttooluse.py @@ -35,11 +35,13 @@ def main(): # Determine event type based on tool tool_name = input_data.get('tool_name', '') - event = None if tool_name == 'Bash': event = 'bash' elif tool_name in ['Edit', 'Write', 'MultiEdit']: event = 'file' + else: + # Unknown tool: only evaluate event=all rules. + event = 'all' # Load rules rules = load_rules(event=event) diff --git a/plugins/hookify/hooks/pretooluse.py b/plugins/hookify/hooks/pretooluse.py index f265c277e3..29f47a549b 100755 --- a/plugins/hookify/hooks/pretooluse.py +++ b/plugins/hookify/hooks/pretooluse.py @@ -42,11 +42,14 @@ def main(): # For PreToolUse, we use tool_name to determine "bash" vs "file" event tool_name = input_data.get('tool_name', '') - event = None if tool_name == 'Bash': event = 'bash' elif tool_name in ['Edit', 'Write', 'MultiEdit']: event = 'file' + else: + # Unknown tool: only evaluate event=all rules to avoid + # bash/file-specific rules running on unrelated tools. + event = 'all' # Load rules rules = load_rules(event=event) diff --git a/scripts/auto-close-duplicates.ts b/scripts/auto-close-duplicates.ts index 2ad3bd3112..cfa8640a18 100644 --- a/scripts/auto-close-duplicates.ts +++ b/scripts/auto-close-duplicates.ts @@ -126,7 +126,7 @@ async function autoCloseDuplicates(): Promise { token ); - if (pageIssues.length === 0) break; + if (pageIssues.length < perPage) break; // Filter for issues created more than 3 days ago const oldEnoughIssues = pageIssues.filter(issue => diff --git a/scripts/backfill-duplicate-comments.ts b/scripts/backfill-duplicate-comments.ts index f79ab43160..bf1a61b310 100644 --- a/scripts/backfill-duplicate-comments.ts +++ b/scripts/backfill-duplicate-comments.ts @@ -107,7 +107,7 @@ Environment Variables: token ); - if (pageIssues.length === 0) break; + if (pageIssues.length < perPage) break; // Filter to only include issues within the specified range const filteredIssues = pageIssues.filter(issue => diff --git a/scripts/sweep.ts b/scripts/sweep.ts index 4709290be3..3d06e24624 100644 --- a/scripts/sweep.ts +++ b/scripts/sweep.ts @@ -42,6 +42,19 @@ async function githubRequest( // -- +async function fetchAllPages(baseEndpoint: string, perPage = 100): Promise { + const all: T[] = []; + for (let page = 1; ; page++) { + const sep = baseEndpoint.includes("?") ? "&" : "?"; + const items = await githubRequest(`${baseEndpoint}${sep}page=${page}`); + all.push(...items); + if (items.length < perPage) break; + } + return all; +} + +// -- + async function markStale(owner: string, repo: string) { const staleDays = lifecycle.find((l) => l.label === "stale")!.days; const cutoff = new Date(); @@ -55,7 +68,7 @@ async function markStale(owner: string, repo: string) { const issues = await githubRequest( `/repos/${owner}/${repo}/issues?state=open&sort=updated&direction=asc&per_page=100&page=${page}` ); - if (issues.length === 0) break; + if (issues.length < 100) break; for (const issue of issues) { if (issue.pull_request) continue; @@ -101,7 +114,7 @@ async function closeExpired(owner: string, repo: string) { const issues = await githubRequest( `/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}` ); - if (issues.length === 0) break; + if (issues.length < 100) break; for (const issue of issues) { if (issue.pull_request) continue; @@ -112,7 +125,7 @@ async function closeExpired(owner: string, repo: string) { const base = `/repos/${owner}/${repo}/issues/${issue.number}`; - const events = await githubRequest(`${base}/events?per_page=100`); + const events = await fetchAllPages(`${base}/events?per_page=100`, 100); const labeledAt = events .filter((e) => e.event === "labeled" && e.label?.name === label) @@ -124,8 +137,9 @@ async function closeExpired(owner: string, repo: string) { // Skip if a non-bot user commented after the label was applied. // The triage workflow should remove lifecycle labels on human // activity, but check here too as a safety net. - const comments = await githubRequest( - `${base}/comments?since=${labeledAt.toISOString()}&per_page=100` + const comments = await fetchAllPages( + `${base}/comments?since=${labeledAt.toISOString()}&per_page=100`, + 100 ); const hasHumanComment = comments.some( (c) => c.user && c.user.type !== "Bot"