From ed4b49d5f742d3a1a832763595be75c349ba6207 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:45:24 +0000 Subject: [PATCH 1/2] Update fuzzy schedule algorithm with weighted preferred time windows and peak avoidance - Add buildWeightedDailyPool(), weightedDailyTimeSlot(), avoidPeakMinutes() helpers - Full-day scatter patterns (DAILY, DAILY_WEEKDAYS, WEEKLY, BI_WEEKLY, TRI_WEEKLY) now use the weighted pool landing in BEST (02-05 UTC, weight 3), GOOD (10-12 UTC, weight 2), or OK (19-23 UTC, weight 1) windows - Targeted scatter patterns (DAILY_AROUND, DAILY_BETWEEN, WEEKLY_AROUND, *_WEEKDAYS) now avoid :30 in hours 06-09 (EU morning peak) and :15/:45 in hours 14-18 (US business) - Update cross-platform consistency test expected values - Add TestScatterScheduleAvoidsEUMorningPeak, TestScatterScheduleAvoidsUSBusinessHours, TestScatterScheduleUsesPreferredWindows Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1f8485ba-80c3-4083-936f-50d8332a7f88 --- .../agent-performance-analyzer.lock.yml | 2 +- .../workflows/agent-persona-explorer.lock.yml | 2 +- .github/workflows/audit-workflows.lock.yml | 2 +- .../claude-code-user-docs-review.lock.yml | 2 +- .../workflows/cli-version-checker.lock.yml | 2 +- .github/workflows/code-simplifier.lock.yml | 2 +- .../constraint-solving-potd.lock.yml | 2 +- .../workflows/copilot-agent-analysis.lock.yml | 2 +- .../copilot-cli-deep-research.lock.yml | 2 +- .../copilot-pr-prompt-analysis.lock.yml | 2 +- .../copilot-session-insights.lock.yml | 2 +- .../daily-assign-issue-to-user.lock.yml | 2 +- .../workflows/daily-cli-performance.lock.yml | 2 +- .../workflows/daily-cli-tools-tester.lock.yml | 2 +- .github/workflows/daily-code-metrics.lock.yml | 2 +- .../daily-community-attribution.lock.yml | 2 +- .../workflows/daily-compiler-quality.lock.yml | 2 +- .github/workflows/daily-doc-healer.lock.yml | 2 +- .github/workflows/daily-doc-updater.lock.yml | 2 +- .../workflows/daily-firewall-report.lock.yml | 2 +- .../workflows/daily-function-namer.lock.yml | 2 +- .../daily-integrity-analysis.lock.yml | 2 +- .../workflows/daily-issues-report.lock.yml | 2 +- .../daily-malicious-code-scan.lock.yml | 2 +- .../daily-multi-device-docs-tester.lock.yml | 2 +- .../daily-observability-report.lock.yml | 2 +- .../daily-performance-summary.lock.yml | 2 +- .github/workflows/daily-regulatory.lock.yml | 2 +- .../daily-rendering-scripts-verifier.lock.yml | 2 +- .../daily-safe-output-integrator.lock.yml | 2 +- .../daily-safe-output-optimizer.lock.yml | 2 +- .../daily-safe-outputs-conformance.lock.yml | 2 +- .../workflows/daily-secrets-analysis.lock.yml | 2 +- .../daily-security-red-team.lock.yml | 2 +- .github/workflows/daily-semgrep-scan.lock.yml | 2 +- .../daily-syntax-error-quality.lock.yml | 2 +- .../daily-team-evolution-insights.lock.yml | 2 +- .../daily-testify-uber-super-expert.lock.yml | 2 +- .../workflows/daily-workflow-updater.lock.yml | 2 +- .github/workflows/dead-code-remover.lock.yml | 2 +- .github/workflows/delight.lock.yml | 2 +- .github/workflows/dependabot-burner.lock.yml | 2 +- .../developer-docs-consolidator.lock.yml | 2 +- .github/workflows/docs-noob-tester.lock.yml | 2 +- .github/workflows/draft-pr-cleanup.lock.yml | 2 +- .../duplicate-code-detector.lock.yml | 2 +- .github/workflows/firewall-escape.lock.yml | 2 +- .../github-remote-mcp-auth-test.lock.yml | 2 +- .github/workflows/go-logger.lock.yml | 2 +- .github/workflows/gpclean.lock.yml | 2 +- .../workflows/instructions-janitor.lock.yml | 2 +- .github/workflows/issue-arborist.lock.yml | 2 +- .github/workflows/jsweep.lock.yml | 2 +- .github/workflows/lockfile-stats.lock.yml | 2 +- .github/workflows/metrics-collector.lock.yml | 2 +- .../prompt-clustering-analysis.lock.yml | 2 +- .github/workflows/safe-output-health.lock.yml | 2 +- .../schema-consistency-checker.lock.yml | 2 +- .../semantic-function-refactor.lock.yml | 2 +- .github/workflows/sergo.lock.yml | 2 +- .../workflows/static-analysis-report.lock.yml | 2 +- .../workflows/step-name-alignment.lock.yml | 2 +- .github/workflows/sub-issue-closer.lock.yml | 2 +- .github/workflows/terminal-stylist.lock.yml | 2 +- .../workflows/ubuntu-image-analyzer.lock.yml | 2 +- .github/workflows/unbloat-docs.lock.yml | 2 +- .github/workflows/update-astro.lock.yml | 2 +- .../weekly-blog-post-writer.lock.yml | 2 +- .../weekly-editors-health-check.lock.yml | 2 +- .../weekly-safe-outputs-spec-review.lock.yml | 2 +- .../workflow-health-manager.lock.yml | 2 +- .../workflows/workflow-normalizer.lock.yml | 2 +- .../workflow-skill-extractor.lock.yml | 2 +- pkg/parser/schedule_fuzzy_scatter.go | 159 +++++++++++------ pkg/parser/schedule_fuzzy_scatter_test.go | 161 ++++++++++++++++++ pkg/parser/schedule_parser_stability_test.go | 8 +- 76 files changed, 341 insertions(+), 133 deletions(-) diff --git a/.github/workflows/agent-performance-analyzer.lock.yml b/.github/workflows/agent-performance-analyzer.lock.yml index 24050e7fcaa..5e66c996129 100644 --- a/.github/workflows/agent-performance-analyzer.lock.yml +++ b/.github/workflows/agent-performance-analyzer.lock.yml @@ -31,7 +31,7 @@ name: "Agent Performance Analyzer - Meta-Orchestrator" "on": schedule: - - cron: "43 20 * * *" + - cron: "43 3 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/agent-persona-explorer.lock.yml b/.github/workflows/agent-persona-explorer.lock.yml index 6d996f0f4d2..47414da38b4 100644 --- a/.github/workflows/agent-persona-explorer.lock.yml +++ b/.github/workflows/agent-persona-explorer.lock.yml @@ -31,7 +31,7 @@ name: "Agent Persona Explorer" "on": schedule: - - cron: "26 15 * * *" + - cron: "37 2 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index a91d6ce3cee..21e4ec75a14 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -33,7 +33,7 @@ name: "Agentic Workflow Audit Agent" "on": schedule: - - cron: "12 16 * * *" + - cron: "48 20 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/claude-code-user-docs-review.lock.yml b/.github/workflows/claude-code-user-docs-review.lock.yml index 10aa92e3fde..edc0d1bc699 100644 --- a/.github/workflows/claude-code-user-docs-review.lock.yml +++ b/.github/workflows/claude-code-user-docs-review.lock.yml @@ -31,7 +31,7 @@ name: "Claude Code User Documentation Review" "on": schedule: - - cron: "13 17 * * *" + - cron: "48 12 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index 5094c424df8..f5d31f0b6a3 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -32,7 +32,7 @@ name: "CLI Version Checker" "on": schedule: - - cron: "45 6 * * *" + - cron: "23 3 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index 584080603d5..63d0c41bf18 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -32,7 +32,7 @@ name: "Code Simplifier" "on": schedule: - - cron: "30 17 * * *" + - cron: "37 5 * * *" # Friendly format: daily (scattered) # skip-if-match: is:pr is:open in:title "[code-simplifier]" # Skip-if-match processed as search check in pre-activation job workflow_dispatch: diff --git a/.github/workflows/constraint-solving-potd.lock.yml b/.github/workflows/constraint-solving-potd.lock.yml index 5ca48355b9a..0c540056add 100644 --- a/.github/workflows/constraint-solving-potd.lock.yml +++ b/.github/workflows/constraint-solving-potd.lock.yml @@ -26,7 +26,7 @@ name: "Constraint Solving — Problem of the Day" "on": schedule: - - cron: "32 16 * * *" + - cron: "31 12 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index 070ed8a6dab..592050ec5df 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -34,7 +34,7 @@ name: "Copilot Agent PR Analysis" "on": schedule: - - cron: "9 10 * * *" + - cron: "19 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/copilot-cli-deep-research.lock.yml b/.github/workflows/copilot-cli-deep-research.lock.yml index 790876f2146..724de4c42ec 100644 --- a/.github/workflows/copilot-cli-deep-research.lock.yml +++ b/.github/workflows/copilot-cli-deep-research.lock.yml @@ -31,7 +31,7 @@ name: "Copilot CLI Deep Research Agent" "on": schedule: - - cron: "50 10 * * *" + - cron: "52 20 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index 18365ab49db..ad9de3af033 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -34,7 +34,7 @@ name: "Copilot PR Prompt Pattern Analysis" "on": schedule: - - cron: "11 10 * * *" + - cron: "7 4 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index f37529bcf52..42f9588a1ff 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -36,7 +36,7 @@ name: "Copilot Session Insights" "on": schedule: - - cron: "47 21 * * *" + - cron: "14 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-assign-issue-to-user.lock.yml b/.github/workflows/daily-assign-issue-to-user.lock.yml index 789786bf33d..9676d5abedc 100644 --- a/.github/workflows/daily-assign-issue-to-user.lock.yml +++ b/.github/workflows/daily-assign-issue-to-user.lock.yml @@ -26,7 +26,7 @@ name: "Auto-Assign Issue" "on": schedule: - - cron: "5 14 * * *" + - cron: "25 23 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-cli-performance.lock.yml b/.github/workflows/daily-cli-performance.lock.yml index 27262d000f1..e7079585e8f 100644 --- a/.github/workflows/daily-cli-performance.lock.yml +++ b/.github/workflows/daily-cli-performance.lock.yml @@ -34,7 +34,7 @@ name: "Daily CLI Performance Agent" # permissions: # Permissions applied to pre-activation job # contents: read schedule: - - cron: "5 22 * * *" + - cron: "7 5 * * *" # Friendly format: daily (scattered) # steps: # Steps injected into pre-activation job # - id: changes diff --git a/.github/workflows/daily-cli-tools-tester.lock.yml b/.github/workflows/daily-cli-tools-tester.lock.yml index 85eb0ad0cb8..44e17edfd51 100644 --- a/.github/workflows/daily-cli-tools-tester.lock.yml +++ b/.github/workflows/daily-cli-tools-tester.lock.yml @@ -31,7 +31,7 @@ name: "Daily CLI Tools Exploratory Tester" "on": schedule: - - cron: "24 4 * * *" + - cron: "37 4 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index 01bb4f73df8..11f4e32ebc4 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -33,7 +33,7 @@ name: "Daily Code Metrics and Trend Tracking Agent" "on": schedule: - - cron: "29 18 * * *" + - cron: "19 21 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-community-attribution.lock.yml b/.github/workflows/daily-community-attribution.lock.yml index 04f2c974bbd..06f6db1b70c 100644 --- a/.github/workflows/daily-community-attribution.lock.yml +++ b/.github/workflows/daily-community-attribution.lock.yml @@ -31,7 +31,7 @@ name: "Daily Community Attribution Updater" "on": schedule: - - cron: "37 6 * * *" + - cron: "27 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-compiler-quality.lock.yml b/.github/workflows/daily-compiler-quality.lock.yml index 94e27acbda2..0f4524cfb90 100644 --- a/.github/workflows/daily-compiler-quality.lock.yml +++ b/.github/workflows/daily-compiler-quality.lock.yml @@ -32,7 +32,7 @@ name: "Daily Compiler Quality Check" "on": schedule: - - cron: "5 0 * * *" + - cron: "23 21 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-doc-healer.lock.yml b/.github/workflows/daily-doc-healer.lock.yml index ce375c32ad6..59e75140e47 100644 --- a/.github/workflows/daily-doc-healer.lock.yml +++ b/.github/workflows/daily-doc-healer.lock.yml @@ -32,7 +32,7 @@ name: "Daily Documentation Healer" "on": schedule: - - cron: "24 8 * * *" + - cron: "51 12 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 7197975dc40..baca63c516e 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -31,7 +31,7 @@ name: "Daily Documentation Updater" "on": schedule: - - cron: "38 18 * * *" + - cron: "14 20 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index 493d64f3f64..19fcb4b7a5b 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -32,7 +32,7 @@ name: "Daily Firewall Logs Collector and Reporter" "on": schedule: - - cron: "37 0 * * *" + - cron: "30 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-function-namer.lock.yml b/.github/workflows/daily-function-namer.lock.yml index 7832b80249f..7ff4b19c074 100644 --- a/.github/workflows/daily-function-namer.lock.yml +++ b/.github/workflows/daily-function-namer.lock.yml @@ -32,7 +32,7 @@ name: "Daily Go Function Namer" "on": schedule: - - cron: "12 5 * * *" + - cron: "53 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-integrity-analysis.lock.yml b/.github/workflows/daily-integrity-analysis.lock.yml index 0ab95249c27..a099b6c9b21 100644 --- a/.github/workflows/daily-integrity-analysis.lock.yml +++ b/.github/workflows/daily-integrity-analysis.lock.yml @@ -32,7 +32,7 @@ name: "Daily DIFC Integrity-Filtered Events Analyzer" "on": schedule: - - cron: "54 9 * * *" + - cron: "54 19 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml index cebdc9c1019..7c8a83681bb 100644 --- a/.github/workflows/daily-issues-report.lock.yml +++ b/.github/workflows/daily-issues-report.lock.yml @@ -36,7 +36,7 @@ name: "Daily Issues Report Generator" "on": schedule: - - cron: "45 20 * * *" + - cron: "6 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index 6b1c3dee083..9ff8c39df60 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -31,7 +31,7 @@ name: "Daily Malicious Code Scan Agent" "on": schedule: - - cron: "24 9 * * *" + - cron: "8 21 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 4d36aa4f916..83a7d93517c 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -32,7 +32,7 @@ name: "Multi-Device Docs Tester" "on": schedule: - - cron: "6 2 * * *" + - cron: "52 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: inputs: diff --git a/.github/workflows/daily-observability-report.lock.yml b/.github/workflows/daily-observability-report.lock.yml index ab3a73134ef..62babba88ac 100644 --- a/.github/workflows/daily-observability-report.lock.yml +++ b/.github/workflows/daily-observability-report.lock.yml @@ -31,7 +31,7 @@ name: "Daily Observability Report for AWF Firewall and MCP Gateway" "on": schedule: - - cron: "14 4 * * *" + - cron: "30 23 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index 5cb35270eeb..a35f77a2588 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -33,7 +33,7 @@ name: "Daily Project Performance Summary Generator (Using MCP Scripts)" "on": schedule: - - cron: "51 8 * * *" + - cron: "13 23 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-regulatory.lock.yml b/.github/workflows/daily-regulatory.lock.yml index 73300f5ce46..ff6d6f8e29c 100644 --- a/.github/workflows/daily-regulatory.lock.yml +++ b/.github/workflows/daily-regulatory.lock.yml @@ -32,7 +32,7 @@ name: "Daily Regulatory Report Generator" "on": schedule: - - cron: "52 23 * * *" + - cron: "54 20 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-rendering-scripts-verifier.lock.yml b/.github/workflows/daily-rendering-scripts-verifier.lock.yml index f723c8af6ca..812d29e6cdd 100644 --- a/.github/workflows/daily-rendering-scripts-verifier.lock.yml +++ b/.github/workflows/daily-rendering-scripts-verifier.lock.yml @@ -32,7 +32,7 @@ name: "Daily Rendering Scripts Verifier" "on": schedule: - - cron: "38 8 * * *" + - cron: "54 10 * * *" # Friendly format: daily (scattered) # skip-if-match: is:pr is:open in:title "[rendering-scripts]" # Skip-if-match processed as search check in pre-activation job workflow_dispatch: diff --git a/.github/workflows/daily-safe-output-integrator.lock.yml b/.github/workflows/daily-safe-output-integrator.lock.yml index 5f2fcc31a6d..32db3fa3561 100644 --- a/.github/workflows/daily-safe-output-integrator.lock.yml +++ b/.github/workflows/daily-safe-output-integrator.lock.yml @@ -31,7 +31,7 @@ name: "Daily Safe Output Integrator" "on": schedule: - - cron: "19 5 * * *" + - cron: "49 22 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-safe-output-optimizer.lock.yml b/.github/workflows/daily-safe-output-optimizer.lock.yml index 0f7a5684861..1a7f518afeb 100644 --- a/.github/workflows/daily-safe-output-optimizer.lock.yml +++ b/.github/workflows/daily-safe-output-optimizer.lock.yml @@ -33,7 +33,7 @@ name: "Daily Safe Output Tool Optimizer" "on": schedule: - - cron: "11 4 * * *" + - cron: "31 23 * * *" # Friendly format: daily (scattered) # skip-if-match: is:issue is:open in:title "[safeoutputs]" # Skip-if-match processed as search check in pre-activation job workflow_dispatch: diff --git a/.github/workflows/daily-safe-outputs-conformance.lock.yml b/.github/workflows/daily-safe-outputs-conformance.lock.yml index 75685c112f1..b454b2171ed 100644 --- a/.github/workflows/daily-safe-outputs-conformance.lock.yml +++ b/.github/workflows/daily-safe-outputs-conformance.lock.yml @@ -31,7 +31,7 @@ name: "Daily Safe Outputs Conformance Checker" "on": schedule: - - cron: "16 15 * * *" + - cron: "48 21 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-secrets-analysis.lock.yml b/.github/workflows/daily-secrets-analysis.lock.yml index 0c5c6e1c7a3..8bfe9e27765 100644 --- a/.github/workflows/daily-secrets-analysis.lock.yml +++ b/.github/workflows/daily-secrets-analysis.lock.yml @@ -31,7 +31,7 @@ name: "Daily Secrets Analysis Agent" "on": schedule: - - cron: "38 4 * * *" + - cron: "6 22 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-security-red-team.lock.yml b/.github/workflows/daily-security-red-team.lock.yml index 9b5035d7b73..6b596e7a15e 100644 --- a/.github/workflows/daily-security-red-team.lock.yml +++ b/.github/workflows/daily-security-red-team.lock.yml @@ -31,7 +31,7 @@ name: "Daily Security Red Team Agent" "on": schedule: - - cron: "34 12 * * *" + - cron: "19 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-semgrep-scan.lock.yml b/.github/workflows/daily-semgrep-scan.lock.yml index e2b8071ebfc..4b0fbaaf523 100644 --- a/.github/workflows/daily-semgrep-scan.lock.yml +++ b/.github/workflows/daily-semgrep-scan.lock.yml @@ -31,7 +31,7 @@ name: "Daily Semgrep Scan" "on": schedule: - - cron: "11 18 * * *" + - cron: "29 23 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-syntax-error-quality.lock.yml b/.github/workflows/daily-syntax-error-quality.lock.yml index 84729dcfaab..474ba14d072 100644 --- a/.github/workflows/daily-syntax-error-quality.lock.yml +++ b/.github/workflows/daily-syntax-error-quality.lock.yml @@ -31,7 +31,7 @@ name: "Daily Syntax Error Quality Check" "on": schedule: - - cron: "38 11 * * *" + - cron: "19 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-team-evolution-insights.lock.yml b/.github/workflows/daily-team-evolution-insights.lock.yml index ce4fe71ccbd..090207f45fe 100644 --- a/.github/workflows/daily-team-evolution-insights.lock.yml +++ b/.github/workflows/daily-team-evolution-insights.lock.yml @@ -31,7 +31,7 @@ name: "Daily Team Evolution Insights" "on": schedule: - - cron: "31 9 * * *" + - cron: "34 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/daily-testify-uber-super-expert.lock.yml b/.github/workflows/daily-testify-uber-super-expert.lock.yml index 93949d80834..f13a9265b19 100644 --- a/.github/workflows/daily-testify-uber-super-expert.lock.yml +++ b/.github/workflows/daily-testify-uber-super-expert.lock.yml @@ -34,7 +34,7 @@ name: "Daily Testify Uber Super Expert" "on": schedule: - - cron: "48 17 * * *" + - cron: "18 11 * * *" # Friendly format: daily (scattered) # skip-if-match: is:issue is:open in:title "[testify-expert]" # Skip-if-match processed as search check in pre-activation job workflow_dispatch: diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml index 387f3a157c1..41d34432c48 100644 --- a/.github/workflows/daily-workflow-updater.lock.yml +++ b/.github/workflows/daily-workflow-updater.lock.yml @@ -27,7 +27,7 @@ name: "Daily Workflow Updater" "on": schedule: - - cron: "17 6 * * *" + - cron: "43 20 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/dead-code-remover.lock.yml b/.github/workflows/dead-code-remover.lock.yml index 1e9d14235c3..41dd78a4529 100644 --- a/.github/workflows/dead-code-remover.lock.yml +++ b/.github/workflows/dead-code-remover.lock.yml @@ -31,7 +31,7 @@ name: "Dead Code Removal Agent" "on": schedule: - - cron: "25 21 * * *" + - cron: "42 11 * * *" # Friendly format: daily (scattered) # skip-if-match: is:pr is:open in:title "[dead-code] " # Skip-if-match processed as search check in pre-activation job workflow_dispatch: diff --git a/.github/workflows/delight.lock.yml b/.github/workflows/delight.lock.yml index abc4a223ae2..f78b9b4558a 100644 --- a/.github/workflows/delight.lock.yml +++ b/.github/workflows/delight.lock.yml @@ -32,7 +32,7 @@ name: "Delight" "on": schedule: - - cron: "25 18 * * *" + - cron: "52 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/dependabot-burner.lock.yml b/.github/workflows/dependabot-burner.lock.yml index 6ca073f9576..bd513941167 100644 --- a/.github/workflows/dependabot-burner.lock.yml +++ b/.github/workflows/dependabot-burner.lock.yml @@ -30,7 +30,7 @@ name: "Dependabot Burner" "on": schedule: - - cron: "27 4 * * 0" + - cron: "43 11 * * 5" # Friendly format: weekly (scattered) workflow_dispatch: diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index d476b78487a..440e708e6d2 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -33,7 +33,7 @@ name: "Developer Documentation Consolidator" "on": schedule: - - cron: "34 4 * * *" + - cron: "52 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index 2202a83975e..3bcc7887c57 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -32,7 +32,7 @@ name: "Documentation Noob Tester" "on": schedule: - - cron: "20 4 * * *" + - cron: "48 12 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/draft-pr-cleanup.lock.yml b/.github/workflows/draft-pr-cleanup.lock.yml index 880a821b0a8..882151fa78e 100644 --- a/.github/workflows/draft-pr-cleanup.lock.yml +++ b/.github/workflows/draft-pr-cleanup.lock.yml @@ -27,7 +27,7 @@ name: "Draft PR Cleanup" "on": schedule: - - cron: "31 19 * * *" + - cron: "51 21 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 5d2468f47b8..a7941c53a2e 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -32,7 +32,7 @@ name: "Duplicate Code Detector" "on": schedule: - - cron: "44 3 * * *" + - cron: "41 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/firewall-escape.lock.yml b/.github/workflows/firewall-escape.lock.yml index e294dcfbc33..e11e4249e40 100644 --- a/.github/workflows/firewall-escape.lock.yml +++ b/.github/workflows/firewall-escape.lock.yml @@ -32,7 +32,7 @@ name: "The Great Escapi" types: - labeled schedule: - - cron: "42 15 * * *" + - cron: "53 5 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/github-remote-mcp-auth-test.lock.yml b/.github/workflows/github-remote-mcp-auth-test.lock.yml index 00d5fbb89ba..f7846f0ef47 100644 --- a/.github/workflows/github-remote-mcp-auth-test.lock.yml +++ b/.github/workflows/github-remote-mcp-auth-test.lock.yml @@ -27,7 +27,7 @@ name: "GitHub Remote MCP Authentication Test" "on": schedule: - - cron: "5 20 * * *" + - cron: "23 3 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index 29040af341b..29d9cc7b033 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -31,7 +31,7 @@ name: "Go Logger Enhancement" "on": schedule: - - cron: "29 14 * * *" + - cron: "5 21 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/gpclean.lock.yml b/.github/workflows/gpclean.lock.yml index a43e7b0453f..8d34a5c720c 100644 --- a/.github/workflows/gpclean.lock.yml +++ b/.github/workflows/gpclean.lock.yml @@ -31,7 +31,7 @@ name: "GPL Dependency Cleaner (gpclean)" "on": schedule: - - cron: "40 6 * * *" + - cron: "7 4 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index b8dc20fcf8e..0c8f5221b8f 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -27,7 +27,7 @@ name: "Instructions Janitor" "on": schedule: - - cron: "29 4 * * *" + - cron: "32 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index 0b99d9003ed..efac1ceb95d 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -32,7 +32,7 @@ name: "Issue Arborist" "on": schedule: - - cron: "54 7 * * *" + - cron: "30 21 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index b02526f559c..9f1ed41688b 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -27,7 +27,7 @@ name: "jsweep - JavaScript Unbloater" "on": schedule: - - cron: "40 21 * * *" + - cron: "37 3 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index 4c1366ed237..4d2252e8cc3 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -31,7 +31,7 @@ name: "Lockfile Statistics Analysis Agent" "on": schedule: - - cron: "21 5 * * *" + - cron: "53 23 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/metrics-collector.lock.yml b/.github/workflows/metrics-collector.lock.yml index aac4730cf30..f741e5fc900 100644 --- a/.github/workflows/metrics-collector.lock.yml +++ b/.github/workflows/metrics-collector.lock.yml @@ -27,7 +27,7 @@ name: "Metrics Collector - Infrastructure Agent" "on": schedule: - - cron: "47 16 * * *" + - cron: "19 19 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml index 630d06786e8..7c51985600f 100644 --- a/.github/workflows/prompt-clustering-analysis.lock.yml +++ b/.github/workflows/prompt-clustering-analysis.lock.yml @@ -35,7 +35,7 @@ name: "Copilot Agent Prompt Clustering Analysis" "on": schedule: - - cron: "40 14 * * *" + - cron: "46 19 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/safe-output-health.lock.yml b/.github/workflows/safe-output-health.lock.yml index 20695554f3c..cc56ef6e522 100644 --- a/.github/workflows/safe-output-health.lock.yml +++ b/.github/workflows/safe-output-health.lock.yml @@ -32,7 +32,7 @@ name: "Safe Output Health Monitor" "on": schedule: - - cron: "32 23 * * *" + - cron: "47 12 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml index 483d8e2e256..18ce6213987 100644 --- a/.github/workflows/schema-consistency-checker.lock.yml +++ b/.github/workflows/schema-consistency-checker.lock.yml @@ -31,7 +31,7 @@ name: "Schema Consistency Checker" "on": schedule: - - cron: "24 16 * * *" + - cron: "37 3 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index 7d47c4092df..d0c3da2300d 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -32,7 +32,7 @@ name: "Semantic Function Refactoring" "on": schedule: - - cron: "44 10 * * *" + - cron: "12 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/sergo.lock.yml b/.github/workflows/sergo.lock.yml index fd91519bd03..770864841d3 100644 --- a/.github/workflows/sergo.lock.yml +++ b/.github/workflows/sergo.lock.yml @@ -32,7 +32,7 @@ name: "Sergo - Serena Go Expert" "on": schedule: - - cron: "8 12 * * *" + - cron: "6 20 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml index 203a72b815f..02686c708c8 100644 --- a/.github/workflows/static-analysis-report.lock.yml +++ b/.github/workflows/static-analysis-report.lock.yml @@ -31,7 +31,7 @@ name: "Static Analysis Report" "on": schedule: - - cron: "32 2 * * *" + - cron: "26 19 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/step-name-alignment.lock.yml b/.github/workflows/step-name-alignment.lock.yml index 42055b2a830..c18e784c54d 100644 --- a/.github/workflows/step-name-alignment.lock.yml +++ b/.github/workflows/step-name-alignment.lock.yml @@ -27,7 +27,7 @@ name: "Step Name Alignment" "on": schedule: - - cron: "30 14 * * *" + - cron: "18 19 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/sub-issue-closer.lock.yml b/.github/workflows/sub-issue-closer.lock.yml index 8c7f1f240e8..8d1e2c65dbd 100644 --- a/.github/workflows/sub-issue-closer.lock.yml +++ b/.github/workflows/sub-issue-closer.lock.yml @@ -27,7 +27,7 @@ name: "Sub-Issue Closer" "on": schedule: - - cron: "27 1 * * *" + - cron: "27 10 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/terminal-stylist.lock.yml b/.github/workflows/terminal-stylist.lock.yml index fb0c9dceb3a..544b1b2b20f 100644 --- a/.github/workflows/terminal-stylist.lock.yml +++ b/.github/workflows/terminal-stylist.lock.yml @@ -32,7 +32,7 @@ name: "Terminal Stylist" "on": schedule: - - cron: "9 17 * * *" + - cron: "39 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/ubuntu-image-analyzer.lock.yml b/.github/workflows/ubuntu-image-analyzer.lock.yml index 6fffa5b11b5..384d64f757a 100644 --- a/.github/workflows/ubuntu-image-analyzer.lock.yml +++ b/.github/workflows/ubuntu-image-analyzer.lock.yml @@ -31,7 +31,7 @@ name: "Ubuntu Actions Image Analyzer" "on": schedule: - - cron: "50 13 * * 3" + - cron: "22 21 * * 4" # Friendly format: weekly (scattered) # skip-if-match: is:pr is:open in:title "[ubuntu-image]" # Skip-if-match processed as search check in pre-activation job workflow_dispatch: diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 1b928595677..a8407a22c91 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -37,7 +37,7 @@ name: "Documentation Unbloat" - created - edited schedule: - - cron: "7 19 * * *" + - cron: "37 2 * * *" workflow_dispatch: null permissions: {} diff --git a/.github/workflows/update-astro.lock.yml b/.github/workflows/update-astro.lock.yml index 36b10d88709..a7af89a2784 100644 --- a/.github/workflows/update-astro.lock.yml +++ b/.github/workflows/update-astro.lock.yml @@ -27,7 +27,7 @@ name: "Update Astro" "on": schedule: - - cron: "37 15 * * *" + - cron: "37 10 * * *" # Friendly format: daily (scattered) # skip-if-no-match: is:pr is:open author:app/dependabot label:dependencies # Skip-if-no-match processed as search check in pre-activation job workflow_dispatch: diff --git a/.github/workflows/weekly-blog-post-writer.lock.yml b/.github/workflows/weekly-blog-post-writer.lock.yml index b8a3fd6d550..cced0b4f5fa 100644 --- a/.github/workflows/weekly-blog-post-writer.lock.yml +++ b/.github/workflows/weekly-blog-post-writer.lock.yml @@ -31,7 +31,7 @@ name: "Weekly Blog Post Writer" "on": schedule: - - cron: "25 20 * * 1" + - cron: "48 12 * * 1" # Friendly format: weekly on monday (scattered) workflow_dispatch: diff --git a/.github/workflows/weekly-editors-health-check.lock.yml b/.github/workflows/weekly-editors-health-check.lock.yml index 8e357b97ddc..9520a4d684a 100644 --- a/.github/workflows/weekly-editors-health-check.lock.yml +++ b/.github/workflows/weekly-editors-health-check.lock.yml @@ -27,7 +27,7 @@ name: "Weekly Editors Health Check" "on": schedule: - - cron: "8 2 * * 5" + - cron: "42 12 * * 6" # Friendly format: weekly (scattered) workflow_dispatch: diff --git a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml index 3424196abbb..c0371c00794 100644 --- a/.github/workflows/weekly-safe-outputs-spec-review.lock.yml +++ b/.github/workflows/weekly-safe-outputs-spec-review.lock.yml @@ -27,7 +27,7 @@ name: "Weekly Safe Outputs Specification Review" "on": schedule: - - cron: "13 3 * * 1" + - cron: "46 10 * * 1" # Friendly format: weekly on monday (scattered) workflow_dispatch: diff --git a/.github/workflows/workflow-health-manager.lock.yml b/.github/workflows/workflow-health-manager.lock.yml index fc34d488ff2..129d9e24b71 100644 --- a/.github/workflows/workflow-health-manager.lock.yml +++ b/.github/workflows/workflow-health-manager.lock.yml @@ -31,7 +31,7 @@ name: "Workflow Health Manager - Meta-Orchestrator" "on": schedule: - - cron: "22 13 * * *" + - cron: "51 11 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/workflow-normalizer.lock.yml b/.github/workflows/workflow-normalizer.lock.yml index 08771a2d5a9..6e8f1f38e8c 100644 --- a/.github/workflows/workflow-normalizer.lock.yml +++ b/.github/workflows/workflow-normalizer.lock.yml @@ -31,7 +31,7 @@ name: "Workflow Normalizer" "on": schedule: - - cron: "53 15 * * *" + - cron: "37 23 * * *" # Friendly format: daily (scattered) workflow_dispatch: diff --git a/.github/workflows/workflow-skill-extractor.lock.yml b/.github/workflows/workflow-skill-extractor.lock.yml index 1165c1c2b03..d18f444a286 100644 --- a/.github/workflows/workflow-skill-extractor.lock.yml +++ b/.github/workflows/workflow-skill-extractor.lock.yml @@ -31,7 +31,7 @@ name: "Workflow Skill Extractor" "on": schedule: - - cron: "48 4 * * 2" + - cron: "11 11 * * 4" # Friendly format: weekly (scattered) workflow_dispatch: diff --git a/pkg/parser/schedule_fuzzy_scatter.go b/pkg/parser/schedule_fuzzy_scatter.go index 6f521a47dc9..c35a8d971c4 100644 --- a/pkg/parser/schedule_fuzzy_scatter.go +++ b/pkg/parser/schedule_fuzzy_scatter.go @@ -14,6 +14,64 @@ var scheduleFuzzyScatterLog = logger.New("parser:schedule_fuzzy_scatter") // This file contains fuzzy schedule scattering logic that deterministically // distributes workflow execution times based on workflow identifiers. +// timeSlot represents a specific (hour, minute) pair used in the weighted daily pool. +type timeSlot struct { + hour int + minute int +} + +// bestDailyMinutes are the "odd" minutes preferred during the BEST tier (02:00–05:59 UTC). +// These low-traffic minutes reduce scheduling collisions with other cron jobs. +var bestDailyMinutes = []int{7, 13, 23, 37, 43, 53} + +// buildWeightedDailyPool constructs the weighted pool of (hour, minute) time slots +// used for full-day scatter patterns. The pool reflects the following distribution: +// +// - BEST (weight 3): 02:00–05:59 UTC at odd minutes (07,13,23,37,43,53) +// - GOOD (weight 2): 10:00–12:59 UTC (gap between EU/US peaks), minutes [5,54] +// - OK (weight 1): 19:00–23:59 UTC (evening hours), minutes [5,54] +// +// Using weights means a randomly selected slot is 3× more likely to land in the +// BEST window than the OK window. +func buildWeightedDailyPool() []timeSlot { + var pool []timeSlot + + // BEST: hours 02–05 at specified odd minutes, weight 3 (appear 3 times each) + for h := 2; h <= 5; h++ { + for _, m := range bestDailyMinutes { + pool = append(pool, timeSlot{h, m}, timeSlot{h, m}, timeSlot{h, m}) + } + } + + // GOOD: hours 10–12, all valid minutes [5,54], weight 2 (appear 2 times each) + for h := 10; h <= 12; h++ { + for m := 5; m <= 54; m++ { + pool = append(pool, timeSlot{h, m}, timeSlot{h, m}) + } + } + + // OK: hours 19–23, all valid minutes [5,54], weight 1 + for h := 19; h <= 23; h++ { + for m := 5; m <= 54; m++ { + pool = append(pool, timeSlot{h, m}) + } + } + + return pool +} + +// weightedDailyPool is the pre-computed weighted pool of daily time slots. +// Pool size: 4×6×3 (BEST) + 3×50×2 (GOOD) + 5×50×1 (OK) = 72 + 300 + 250 = 622 slots. +var weightedDailyPool = buildWeightedDailyPool() + +// weightedDailyTimeSlot returns a deterministic (hour, minute) pair sampled from the +// weighted daily time slot pool for the given workflow identifier. +// All returned slots are already within the preferred windows and have valid minutes. +func weightedDailyTimeSlot(identifier string) (int, int) { + slot := weightedDailyPool[stableHash(identifier, len(weightedDailyPool))] + return slot.hour, slot.minute +} + // avoidHourBoundary remaps a minute value to avoid the 5-minute window before // and after each hour (minutes 0–4 and 55–59). These windows are subject to // usage peaks on GitHub Actions, especially at 00:00 UTC. @@ -32,6 +90,30 @@ func avoidHourBoundary(minute int) int { return minute } +// avoidPeakMinutes shifts minute values that fall within known high-traffic windows: +// +// - EU morning peak (06:00–09:59 UTC): avoids :30 minute (shifted to :31) +// - US business hours (14:00–18:59 UTC): avoids :15 and :45 minutes (shifted to :16 / :46) +// +// All replacement values stay within [5, 54]. This is applied after avoidHourBoundary +// for targeted-scatter patterns where the hour is determined by a user-specified target. +func avoidPeakMinutes(hour, minute int) int { + // EU morning peak: avoid :30 in hours 06–09 + if hour >= 6 && hour <= 9 && minute == 30 { + return 31 + } + // US business hours (moderate): avoid :15 and :45 in hours 14–18 + if hour >= 14 && hour <= 18 { + if minute == 15 { + return 16 + } + if minute == 45 { + return 46 + } + } + return minute +} + // stableHash returns a deterministic hash value in the range [0, modulo) // using FNV-1a hash algorithm, which is stable across platforms and Go versions. func stableHash(s string, modulo int) int { @@ -103,7 +185,7 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { } hour := scatteredMinutes / 60 - minute := avoidHourBoundary(scatteredMinutes % 60) + minute := avoidPeakMinutes(hour, avoidHourBoundary(scatteredMinutes%60)) result := fmt.Sprintf("%d %d * * 1-5", minute, hour) scheduleFuzzyScatterLog.Printf("FUZZY:DAILY_AROUND_WEEKDAYS scattered: original=%d:%d, scattered=%d:%d, result=%s", targetHour, targetMinute, hour, minute, result) @@ -172,7 +254,7 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { } hour := scatteredMinutes / 60 - minute := avoidHourBoundary(scatteredMinutes % 60) + minute := avoidPeakMinutes(hour, avoidHourBoundary(scatteredMinutes%60)) result := fmt.Sprintf("%d %d * * 1-5", minute, hour) scheduleFuzzyScatterLog.Printf("FUZZY:DAILY_BETWEEN_WEEKDAYS scattered: start=%d:%d, end=%d:%d, scattered=%d:%d, result=%s", startHour, startMinute, endHour, endMinute, hour, minute, result) @@ -229,7 +311,7 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { } hour := scatteredMinutes / 60 - minute := avoidHourBoundary(scatteredMinutes % 60) + minute := avoidPeakMinutes(hour, avoidHourBoundary(scatteredMinutes%60)) result := fmt.Sprintf("%d %d * * *", minute, hour) scheduleFuzzyScatterLog.Printf("FUZZY:DAILY_AROUND scattered: original=%d:%d, scattered=%d:%d, result=%s", targetHour, targetMinute, hour, minute, result) @@ -298,7 +380,7 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { } hour := scatteredMinutes / 60 - minute := avoidHourBoundary(scatteredMinutes % 60) + minute := avoidPeakMinutes(hour, avoidHourBoundary(scatteredMinutes%60)) result := fmt.Sprintf("%d %d * * *", minute, hour) scheduleFuzzyScatterLog.Printf("FUZZY:DAILY_BETWEEN scattered: start=%d:%d, end=%d:%d, scattered=%d:%d, result=%s", startHour, startMinute, endHour, endMinute, hour, minute, result) @@ -306,32 +388,22 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { return result, nil } - // For FUZZY:DAILY_WEEKDAYS * * *, we scatter across 24 hours on weekdays only + // For FUZZY:DAILY_WEEKDAYS * * *, scatter across the preferred daily time windows on weekdays if strings.HasPrefix(fuzzyCron, "FUZZY:DAILY_WEEKDAYS") { - // Use 24*50 slots (50 valid minutes per hour, avoiding the 5-minute - // window around each hour boundary) to get a deterministic time. - hash := stableHash(workflowIdentifier, 24*50) - - hour := hash / 50 - minute := (hash % 50) + 5 // minutes in [5, 54] + hour, minute := weightedDailyTimeSlot(workflowIdentifier) result := fmt.Sprintf("%d %d * * 1-5", minute, hour) - scheduleFuzzyScatterLog.Printf("FUZZY:DAILY_WEEKDAYS scattered: hash=%d, result=%s", hash, result) + scheduleFuzzyScatterLog.Printf("FUZZY:DAILY_WEEKDAYS scattered: result=%s", result) // Return scattered daily cron with weekday restriction: minute hour * * 1-5 return result, nil } - // For FUZZY:DAILY * * *, we scatter across 24 hours + // For FUZZY:DAILY * * *, scatter across the preferred daily time windows if strings.HasPrefix(fuzzyCron, "FUZZY:DAILY") { - // Use 24*50 slots (50 valid minutes per hour, avoiding the 5-minute - // window around each hour boundary) to get a deterministic time. - hash := stableHash(workflowIdentifier, 24*50) - - hour := hash / 50 - minute := (hash % 50) + 5 // minutes in [5, 54] + hour, minute := weightedDailyTimeSlot(workflowIdentifier) result := fmt.Sprintf("%d %d * * *", minute, hour) - scheduleFuzzyScatterLog.Printf("FUZZY:DAILY scattered: hash=%d, result=%s", hash, result) + scheduleFuzzyScatterLog.Printf("FUZZY:DAILY scattered: result=%s", result) // Return scattered daily cron: minute hour * * * return result, nil } @@ -436,7 +508,7 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { } hour := scatteredMinutes / 60 - minute := avoidHourBoundary(scatteredMinutes % 60) + minute := avoidPeakMinutes(hour, avoidHourBoundary(scatteredMinutes%60)) result := fmt.Sprintf("%d %d * * %s", minute, hour, weekday) scheduleFuzzyScatterLog.Printf("FUZZY:WEEKLY_AROUND scattered: weekday=%s, target=%d:%d, scattered=%d:%d, result=%s", weekday, targetHour, targetMinute, hour, minute, result) @@ -455,30 +527,19 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { weekdayPart := strings.TrimPrefix(parts[0], "FUZZY:WEEKLY:") weekday := weekdayPart - // Use 24*50 slots (50 valid minutes per hour, avoiding the 5-minute - // window around each hour boundary) to get a deterministic time. - hash := stableHash(workflowIdentifier, 24*50) - - hour := hash / 50 - minute := (hash % 50) + 5 // minutes in [5, 54] + hour, minute := weightedDailyTimeSlot(workflowIdentifier) result := fmt.Sprintf("%d %d * * %s", minute, hour, weekday) - scheduleFuzzyScatterLog.Printf("FUZZY:WEEKLY:%s scattered: hash=%d, result=%s", weekday, hash, result) + scheduleFuzzyScatterLog.Printf("FUZZY:WEEKLY:%s scattered: result=%s", weekday, result) // Return scattered weekly cron: minute hour * * DOW return result, nil } - // For FUZZY:WEEKLY * * *, we scatter across all weekdays and times + // For FUZZY:WEEKLY * * *, scatter the weekday deterministically and pick a + // preferred time from the weighted daily pool. if strings.HasPrefix(fuzzyCron, "FUZZY:WEEKLY") { - // Use 7 * 24 * 50 slots (50 valid minutes per hour, avoiding the 5-minute - // window around each hour boundary) to get a deterministic weekday and time. - hash := stableHash(workflowIdentifier, 7*24*50) - - // Each "day block" contains 24*50 = 1200 slots. - weekday := hash / (24 * 50) // Which day of the week (0-6) - slotInDay := hash % (24 * 50) // Which slot of that day (0-1199) - hour := slotInDay / 50 - minute := (slotInDay % 50) + 5 // minutes in [5, 54] + weekday := stableHash(workflowIdentifier, 7) // Which day of the week (0-6) + hour, minute := weightedDailyTimeSlot(workflowIdentifier) result := fmt.Sprintf("%d %d * * %d", minute, hour, weekday) scheduleFuzzyScatterLog.Printf("FUZZY:WEEKLY scattered: weekday=%d, time=%d:%d, result=%s", weekday, hour, minute, result) @@ -486,16 +547,9 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { return result, nil } - // For FUZZY:BI_WEEKLY * * *, we scatter across 2 weeks (14 days) + // For FUZZY:BI_WEEKLY * * *, schedule every 14 days at a preferred time if strings.HasPrefix(fuzzyCron, "FUZZY:BI_WEEKLY") { - // Use 14 * 24 * 50 slots (50 valid minutes per hour, avoiding the 5-minute - // window around each hour boundary) to get a deterministic time. - hash := stableHash(workflowIdentifier, 14*24*50) - - // Extract time within a day using 50-slot per hour mapping. - slotInDay := hash % (24 * 50) // Which slot of the day (0-1199) - hour := slotInDay / 50 - minute := (slotInDay % 50) + 5 // minutes in [5, 54] + hour, minute := weightedDailyTimeSlot(workflowIdentifier) result := fmt.Sprintf("%d %d */%d * *", minute, hour, 14) scheduleFuzzyScatterLog.Printf("FUZZY:BI_WEEKLY scattered: time=%d:%d, result=%s", hour, minute, result) @@ -504,16 +558,9 @@ func ScatterSchedule(fuzzyCron, workflowIdentifier string) (string, error) { return result, nil } - // For FUZZY:TRI_WEEKLY * * *, we scatter across 3 weeks (21 days) + // For FUZZY:TRI_WEEKLY * * *, schedule every 21 days at a preferred time if strings.HasPrefix(fuzzyCron, "FUZZY:TRI_WEEKLY") { - // Use 21 * 24 * 50 slots (50 valid minutes per hour, avoiding the 5-minute - // window around each hour boundary) to get a deterministic time. - hash := stableHash(workflowIdentifier, 21*24*50) - - // Extract time within a day using 50-slot per hour mapping. - slotInDay := hash % (24 * 50) // Which slot of the day (0-1199) - hour := slotInDay / 50 - minute := (slotInDay % 50) + 5 // minutes in [5, 54] + hour, minute := weightedDailyTimeSlot(workflowIdentifier) result := fmt.Sprintf("%d %d */%d * *", minute, hour, 21) scheduleFuzzyScatterLog.Printf("FUZZY:TRI_WEEKLY scattered: time=%d:%d, result=%s", hour, minute, result) diff --git a/pkg/parser/schedule_fuzzy_scatter_test.go b/pkg/parser/schedule_fuzzy_scatter_test.go index b498c30f740..05c4176240b 100644 --- a/pkg/parser/schedule_fuzzy_scatter_test.go +++ b/pkg/parser/schedule_fuzzy_scatter_test.go @@ -690,3 +690,164 @@ func TestScatterScheduleAvoidsHourBoundary(t *testing.T) { } } } + +// TestScatterScheduleAvoidsEUMorningPeak verifies that targeted-scatter patterns +// never produce minute :30 during EU morning peak hours (06:00–09:59 UTC). +func TestScatterScheduleAvoidsEUMorningPeak(t *testing.T) { + workflowIDs := []string{ + "workflow-a.md", "workflow-b.md", "workflow-c.md", + "test-workflow", "daily-security-scan", "weekly-report", + "my-org/my-repo/my-workflow.md", "scanner-job", + } + + // Targeted patterns that scatter around EU morning peak hours. + patterns := []string{ + "FUZZY:DAILY_AROUND:7:0 * * *", + "FUZZY:DAILY_AROUND:7:30 * * *", + "FUZZY:DAILY_AROUND:8:0 * * *", + "FUZZY:DAILY_AROUND:9:0 * * *", + "FUZZY:DAILY_AROUND_WEEKDAYS:7:0 * * *", + "FUZZY:DAILY_AROUND_WEEKDAYS:8:30 * * *", + "FUZZY:DAILY_BETWEEN:6:0:10:0 * * *", + "FUZZY:DAILY_BETWEEN_WEEKDAYS:6:0:10:0 * * *", + "FUZZY:WEEKLY_AROUND:1:7:0 * * *", + "FUZZY:WEEKLY_AROUND:3:8:30 * * *", + } + + for _, pattern := range patterns { + for _, wfID := range workflowIDs { + t.Run(pattern+"/"+wfID, func(t *testing.T) { + result, err := ScatterSchedule(pattern, wfID) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + fields := strings.Fields(result) + if len(fields) != 5 { + t.Fatalf("expected 5 cron fields, got %d: %s", len(fields), result) + } + + var minute, hour int + if _, scanErr := fmt.Sscanf(fields[0], "%d", &minute); scanErr != nil { + t.Fatalf("could not parse minute from cron %q: %v", result, scanErr) + } + if _, scanErr := fmt.Sscanf(fields[1], "%d", &hour); scanErr != nil { + // Hour field may be a range or wildcard for some patterns – skip check. + return + } + + if hour >= 6 && hour <= 9 && minute == 30 { + t.Errorf("pattern=%q wfID=%q: cron %q schedules at :30 during EU morning peak (hours 06-09 UTC)", + pattern, wfID, result) + } + }) + } + } +} + +// TestScatterScheduleAvoidsUSBusinessHours verifies that targeted-scatter patterns +// never produce :15 or :45 minutes during US business hours (14:00–18:59 UTC). +func TestScatterScheduleAvoidsUSBusinessHours(t *testing.T) { + workflowIDs := []string{ + "workflow-a.md", "workflow-b.md", "workflow-c.md", + "test-workflow", "daily-security-scan", "weekly-report", + "my-org/my-repo/my-workflow.md", "scanner-job", + } + + // Targeted patterns that scatter around US business hours. + patterns := []string{ + "FUZZY:DAILY_AROUND:14:0 * * *", + "FUZZY:DAILY_AROUND:15:15 * * *", + "FUZZY:DAILY_AROUND:16:45 * * *", + "FUZZY:DAILY_AROUND:17:0 * * *", + "FUZZY:DAILY_AROUND:18:0 * * *", + "FUZZY:DAILY_AROUND_WEEKDAYS:15:0 * * *", + "FUZZY:DAILY_AROUND_WEEKDAYS:16:45 * * *", + "FUZZY:DAILY_BETWEEN:14:0:19:0 * * *", + "FUZZY:DAILY_BETWEEN_WEEKDAYS:14:0:19:0 * * *", + "FUZZY:WEEKLY_AROUND:2:15:15 * * *", + "FUZZY:WEEKLY_AROUND:4:17:0 * * *", + } + + for _, pattern := range patterns { + for _, wfID := range workflowIDs { + t.Run(pattern+"/"+wfID, func(t *testing.T) { + result, err := ScatterSchedule(pattern, wfID) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + fields := strings.Fields(result) + if len(fields) != 5 { + t.Fatalf("expected 5 cron fields, got %d: %s", len(fields), result) + } + + var minute, hour int + if _, scanErr := fmt.Sscanf(fields[0], "%d", &minute); scanErr != nil { + t.Fatalf("could not parse minute from cron %q: %v", result, scanErr) + } + if _, scanErr := fmt.Sscanf(fields[1], "%d", &hour); scanErr != nil { + // Hour field may be a range or wildcard for some patterns – skip check. + return + } + + if hour >= 14 && hour <= 18 && (minute == 15 || minute == 45) { + t.Errorf("pattern=%q wfID=%q: cron %q schedules at :%02d during US business hours (hours 14-18 UTC)", + pattern, wfID, result, minute) + } + }) + } + } +} + +// TestScatterScheduleUsesPreferredWindows verifies that full-day scatter patterns +// (FUZZY:DAILY, FUZZY:DAILY_WEEKDAYS, FUZZY:WEEKLY, etc.) land exclusively in the +// preferred time windows: BEST (02–05 UTC), GOOD (10–12 UTC), or OK (19–23 UTC). +func TestScatterScheduleUsesPreferredWindows(t *testing.T) { + workflowIDs := []string{ + "workflow-a.md", "workflow-b.md", "workflow-c.md", + "test-workflow", "daily-security-scan", "weekly-report", + "hourly-checker", "my-org/my-repo/my-workflow.md", + "alpha", "beta", "gamma", "delta", "epsilon", + } + + patterns := []string{ + "FUZZY:DAILY * * *", + "FUZZY:DAILY_WEEKDAYS * * *", + "FUZZY:WEEKLY * * *", + "FUZZY:WEEKLY:1 * * *", + "FUZZY:WEEKLY:5 * * *", + "FUZZY:BI_WEEKLY * * *", + "FUZZY:TRI_WEEKLY * * *", + } + + isInPreferredWindow := func(hour int) bool { + return (hour >= 2 && hour <= 5) || (hour >= 10 && hour <= 12) || (hour >= 19 && hour <= 23) + } + + for _, pattern := range patterns { + for _, wfID := range workflowIDs { + t.Run(pattern+"/"+wfID, func(t *testing.T) { + result, err := ScatterSchedule(pattern, wfID) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + fields := strings.Fields(result) + if len(fields) != 5 { + t.Fatalf("expected 5 cron fields, got %d: %s", len(fields), result) + } + + var hour int + if _, scanErr := fmt.Sscanf(fields[1], "%d", &hour); scanErr != nil { + t.Fatalf("could not parse hour from cron %q: %v", result, scanErr) + } + + if !isInPreferredWindow(hour) { + t.Errorf("pattern=%q wfID=%q: cron %q schedules at hour %d, which is not in a preferred window (02-05, 10-12, or 19-23 UTC)", + pattern, wfID, result, hour) + } + }) + } + } +} diff --git a/pkg/parser/schedule_parser_stability_test.go b/pkg/parser/schedule_parser_stability_test.go index 341ab5e8061..394c44833e8 100644 --- a/pkg/parser/schedule_parser_stability_test.go +++ b/pkg/parser/schedule_parser_stability_test.go @@ -57,13 +57,13 @@ func TestScatterScheduleCrossPlatformConsistency(t *testing.T) { name: "daily - workflow-a.md", fuzzyCron: "FUZZY:DAILY * * *", workflowIdentifier: "workflow-a.md", - expectedCron: "46 14 * * *", + expectedCron: "23 2 * * *", }, { name: "daily - workflow-b.md", fuzzyCron: "FUZZY:DAILY * * *", workflowIdentifier: "workflow-b.md", - expectedCron: "37 1 * * *", + expectedCron: "41 22 * * *", }, { name: "hourly/1 - workflow-a.md", @@ -81,13 +81,13 @@ func TestScatterScheduleCrossPlatformConsistency(t *testing.T) { name: "weekly - workflow-a.md", fuzzyCron: "FUZZY:WEEKLY * * *", workflowIdentifier: "workflow-a.md", - expectedCron: "46 14 * * 0", + expectedCron: "23 2 * * 6", }, { name: "weekly:1 - workflow-a.md", fuzzyCron: "FUZZY:WEEKLY:1 * * *", workflowIdentifier: "workflow-a.md", - expectedCron: "46 14 * * 1", + expectedCron: "23 2 * * 1", }, { name: "daily around 14:00 - workflow-a.md", From 7e20fb3ad825aea53405dafbe16a16929212c95c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:44:09 +0000 Subject: [PATCH 2/2] Widen peak-minute avoidance to 3-minute radius and update spec to v1.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - avoidPeakMinutes: EU peak (hours 06-09) avoids minutes [27,33] → 34 - avoidPeakMinutes: US peak (hours 14-18) avoids [12,18]→19 and [42,48]→49 - Update test assertions to verify ±3 window instead of exact minutes - Update fuzzy-schedule-specification.md to v1.2.0: - Section 6.3.1: weighted daily pool algorithm - Sections 6.3.5-6.3.6: weekly/bi/tri use same pool - New Section 6.4: Peak Minutes Avoidance with avoidHourBoundary and avoidPeakMinutes - New compliance tests T-SCATTER-011..016 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/cf1ee0c9-40b1-433a-b6b0-f79297a22c8e --- .../reference/fuzzy-schedule-specification.md | 145 +++++++++++++----- pkg/parser/schedule_fuzzy_scatter.go | 25 +-- pkg/parser/schedule_fuzzy_scatter_test.go | 26 +++- 3 files changed, 142 insertions(+), 54 deletions(-) diff --git a/docs/src/content/docs/reference/fuzzy-schedule-specification.md b/docs/src/content/docs/reference/fuzzy-schedule-specification.md index c919099f673..19ae213f999 100644 --- a/docs/src/content/docs/reference/fuzzy-schedule-specification.md +++ b/docs/src/content/docs/reference/fuzzy-schedule-specification.md @@ -7,7 +7,7 @@ sidebar: # Fuzzy Schedule Time Syntax Specification -**Version**: 1.1.0 +**Version**: 1.2.0 **Status**: Draft Specification **Latest Version**: [fuzzy-schedule-specification](/gh-aw/reference/fuzzy-schedule-specification/) **Editor**: GitHub Agentic Workflows Team @@ -547,18 +547,25 @@ This format ensures workflows with the same filename in different repositories r #### 6.3.1 Daily Schedule Scattering -For `FUZZY:DAILY * * *`: +For `FUZZY:DAILY * * *` and `FUZZY:DAILY_WEEKDAYS * * *`, an implementation MUST use the **weighted daily time slot pool** to select execution time: -1. Calculate hash modulo 1440 (24 hours * 60 minutes) -2. Convert result to hour and minute components -3. Generate cron: ` * * *` +1. Construct a weighted pool of (hour, minute) time slots using three preference tiers: + - **BEST** (weight 3): hours 02–05 UTC, odd minutes `{7, 13, 23, 37, 43, 53}` → 72 slots + - **GOOD** (weight 2): hours 10–12 UTC, minutes `[5, 54]` → 300 slots + - **OK** (weight 1): hours 19–23 UTC, minutes `[5, 54]` → 250 slots + - Total pool size: 622 slots +2. Select slot: `index = hash(workflow_identifier) % pool_size` +3. Extract `(hour, minute)` from the selected slot +4. Generate cron: ` * * *` (or `* * 1-5` for weekday variant) + +The pool is pre-computed once. Because each tier appears proportionally in the pool, a randomly selected slot is 3× more likely to land in the BEST window than in the OK window. **Example**: ``` -hash("github/gh-aw/workflow.md") % 1440 = 343 -hour = 343 / 60 = 5 -minute = 343 % 60 = 43 -cron = "43 5 * * *" (5:43 AM) +pool_size = 622 +hash("github/gh-aw/workflow.md") % 622 = 84 +slot[84] = (hour=2, minute=23) # BEST tier +cron = "23 2 * * *" (2:23 AM UTC) ``` #### 6.3.2 Daily Around Scattering @@ -641,45 +648,90 @@ cron = "53 */2 * * *" (runs at minute 53 every 2 hours) #### 6.3.5 Weekly Schedule Scattering -For `FUZZY:WEEKLY * * *`: - -1. Calculate hash modulo (7 * 24 * 60) = 10080 (week in minutes) -2. Extract day-of-week: `day = (hash_result / 1440) % 7` -3. Extract time: `time_in_minutes = hash_result % 1440` -4. Convert time to hour and minute -5. Generate cron: ` * * ` +For `FUZZY:WEEKLY * * *` and `FUZZY:WEEKLY:DOW * * *`: -For `FUZZY:WEEKLY:DOW * * DOW`: +1. Select day-of-week: `weekday = hash(workflow_identifier) % 7` (0=Sunday, 6=Saturday) + For `FUZZY:WEEKLY:DOW`, the day is fixed from the expression instead. +2. Select time from the **weighted daily time slot pool** (Section 6.3.1) +3. Generate cron: ` * * ` -1. Day is fixed from expression -2. Calculate hash modulo 1440 (day in minutes) -3. Convert to hour and minute -4. Generate cron: ` * * ` +Both patterns use the same weighted pool as the daily schedule, ensuring execution times prefer the BEST/GOOD/OK tiers rather than distributing flatly across the full day. **Example**: ``` weekly on monday day = 1 (Monday) -hash % 1440 = 343 -hour = 5, minute = 43 -cron = "43 5 * * 1" (Monday 5:43 AM) +pool selection → (hour=2, minute=23) # BEST tier +cron = "23 2 * * 1" (Monday 2:23 AM UTC) ``` #### 6.3.6 Bi-weekly and Tri-weekly Scattering -For `FUZZY:BI-WEEKLY * * *`: +For `FUZZY:BI-WEEKLY * * *` and `FUZZY:TRI-WEEKLY * * *`: + +1. Select time from the **weighted daily time slot pool** (Section 6.3.1) +2. Generate cron: ` */14 * *` (bi-weekly) or ` */21 * *` (tri-weekly) + +Both patterns use the same weighted pool to ensure execution during preferred low-traffic windows. + +### 6.4 Peak Minutes Avoidance + +To reduce scheduling collisions with other commonly-scheduled cron jobs, implementations MUST apply two minute-avoidance passes after computing the raw scattered minute value. + +#### 6.4.1 Hour Boundary Avoidance (`avoidHourBoundary`) + +Minutes near the hour boundary (0–4 and 55–59) are subject to elevated load on GitHub Actions infrastructure, especially at 00:00 UTC. + +An implementation MUST remap minute values as follows: + +| Input range | Output | +|-------------|--------| +| [0, 4] | minute + 5 | +| [55, 59] | minute − 5 | +| [5, 54] | unchanged | + +This ensures all generated minute values are in [5, 54]. -1. Calculate hash modulo 1440 -2. Convert to hour and minute -3. Generate cron: ` */14 * *` +**Scope**: Applied to ALL targeted-scatter patterns (DAILY_AROUND, DAILY_BETWEEN, WEEKLY_AROUND, and their weekday variants). -For `FUZZY:TRI-WEEKLY * * *`: +#### 6.4.2 Peak Minutes Avoidance (`avoidPeakMinutes`) -1. Calculate hash modulo 1440 -2. Convert to hour and minute -3. Generate cron: ` */21 * *` +Known high-traffic periods require avoidance of minutes that fall within ±3 of the peak minute values. -### 6.4 Algorithm Requirements +An implementation MUST apply the following remapping **after** `avoidHourBoundary`: + +| Condition | Avoid range | Replacement | +|-----------|-------------|-------------| +| hour ∈ [6, 9] AND minute ∈ [27, 33] | [27, 33] | 34 | +| hour ∈ [14, 18] AND minute ∈ [12, 18] | [12, 18] | 19 | +| hour ∈ [14, 18] AND minute ∈ [42, 48] | [42, 48] | 49 | + +**Rationale**: +- **EU morning peak** (06:00–09:59 UTC): `:30` is a commonly-used cron minute. Staying 3 minutes away (avoiding [27,33]) reduces collisions. +- **US business hours** (14:00–18:59 UTC): `:15` and `:45` are quarter-hour marks widely used by monitoring and reporting cron jobs. Staying 3 minutes away (avoiding [12,18] and [42,48]) reduces collisions. + +**Application order**: `avoidHourBoundary` MUST be applied before `avoidPeakMinutes`. + +**Scope**: `avoidPeakMinutes` applies only to targeted-scatter patterns. Full-day scatter patterns that use the weighted pool (Section 6.3.1) already avoid peak windows by construction, since the pool does not include EU peak hours (06–09) or US peak hours (14–18). + +**Example**: +``` +FUZZY:DAILY_AROUND:14:00, workflow "my-scanner" + Raw scattered time: 14:28 + Step 1 (avoidHourBoundary): 28 → 28 (no change; 28 ∈ [5,54]) + Step 2 (avoidPeakMinutes): 28 → 34 (shifted; hour ∈ [14,18], minute 28 ∈ [27,33] + — wait, hour=14, so EU rule doesn't apply; + US :15 rule: 28 ∉ [12,18]; :45 rule: 28 ∉ [42,48]) + → no shift needed; result: 14:28 + +FUZZY:DAILY_AROUND:15:00, workflow "my-monitor" + Raw scattered time: 15:13 + Step 1 (avoidHourBoundary): 13 → 13 (no change) + Step 2 (avoidPeakMinutes): 13 → 19 (shifted; hour ∈ [14,18], minute 13 ∈ [12,18]) + → result: 15:19 +``` + +### 6.5 Algorithm Requirements An implementation MUST ensure: @@ -687,6 +739,8 @@ An implementation MUST ensure: 2. Modulo operations use consistent integer division 3. Day wrapping uses consistent addition/subtraction rules 4. Minute and hour extraction uses consistent division and modulo operations +5. `avoidHourBoundary` is applied before `avoidPeakMinutes` for all targeted-scatter patterns +6. Full-day scatter patterns use the weighted daily time slot pool (Section 6.3.1) --- @@ -879,13 +933,19 @@ A conforming implementation MUST pass all Level 1 tests. Implementations claimin - **T-SCATTER-001**: Hash produces same output for same input - **T-SCATTER-002**: Different inputs produce different outputs - **T-SCATTER-003**: Hash value is within modulo range (0 to modulo-1) -- **T-SCATTER-004**: Daily schedule scatters across full 24-hour period +- **T-SCATTER-004**: Daily schedule selects time from weighted pool (BEST/GOOD/OK tiers only) - **T-SCATTER-005**: Around schedule stays within ±60 minute window - **T-SCATTER-006**: Between schedule stays within specified range - **T-SCATTER-007**: Midnight-crossing range handles day wrap correctly -- **T-SCATTER-008**: Hourly schedule produces minute 0-59 +- **T-SCATTER-008**: Hourly schedule produces minute in [5, 54] - **T-SCATTER-009**: Weekly schedule selects valid day 0-6 - **T-SCATTER-010**: Same workflow gets same time across compilations +- **T-SCATTER-011**: Daily schedule lands in BEST (02–05), GOOD (10–12), or OK (19–23) window +- **T-SCATTER-012**: Minute values in [5, 54] for all patterns (hour-boundary avoidance) +- **T-SCATTER-013**: DAILY_AROUND scatter landing in EU peak hours (06–09) avoids minutes [27, 33] +- **T-SCATTER-014**: DAILY_AROUND scatter landing in US business hours (14–18) avoids minutes [12, 18] and [42, 48] +- **T-SCATTER-015**: Weekly schedule uses weighted daily time pool (preferred windows) +- **T-SCATTER-016**: Bi-weekly and tri-weekly schedules use weighted daily time pool #### 9.2.7 Cron Generation Tests (Level 1-3) @@ -921,6 +981,10 @@ A conforming implementation MUST pass all Level 1 tests. Implementations claimin | Parse interval schedules | T-INTERVAL-001, 002 | 3 | Required | | Hash determinism | T-SCATTER-001, 002 | 1 | Required | | Scattering distribution | T-SCATTER-004-009 | 1-3 | Required | +| Weighted daily pool | T-SCATTER-011, 015, 016 | 1-3 | Required | +| Peak avoidance (hour boundary) | T-SCATTER-012 | 1-3 | Required | +| Peak avoidance (EU morning peak) | T-SCATTER-013 | 2-3 | Required | +| Peak avoidance (US business hours) | T-SCATTER-014 | 2-3 | Required | | Generate valid cron | T-CRON-001-006 | 1-3 | Required | ### 9.4 Test Execution @@ -1092,6 +1156,17 @@ Implementations MUST validate all user inputs before processing: ## Change Log +### Version 1.2.0 (Draft) + +- **Changed**: Section 6.3.1 — Replaced flat hash-modulo-1440 daily scatter with a **622-entry weighted daily time slot pool** (BEST 02–05 UTC ×3, GOOD 10–12 UTC ×2, OK 19–23 UTC ×1) +- **Changed**: Sections 6.3.5–6.3.6 — Weekly, bi-weekly, and tri-weekly scatter now uses the same weighted pool as the daily schedule +- **Added**: Section 6.4 — **Peak Minutes Avoidance** documenting: + - `avoidHourBoundary`: shifts minutes [0,4]→[5,9] and [55,59]→[50,54] + - `avoidPeakMinutes`: EU peak (hours 06–09) avoids ±3 min of :30 (shifts [27,33]→34); US business hours (14–18) avoids ±3 min of :15 (shifts [12,18]→19) and ±3 min of :45 (shifts [42,48]→49) +- **Renumbered**: Section 6.4 (Algorithm Requirements) → Section 6.5 +- **Added**: Compliance tests T-SCATTER-011 through T-SCATTER-016 covering weighted pool behaviour and peak avoidance +- **Updated**: Compliance checklist (Section 9.3) with new required rows for weighted pool and peak avoidance + ### Version 1.1.0 (Draft) - **Changed**: Hash function requirement relaxed from MUST to SHOULD for FNV-1a diff --git a/pkg/parser/schedule_fuzzy_scatter.go b/pkg/parser/schedule_fuzzy_scatter.go index c35a8d971c4..52f39bcc5d9 100644 --- a/pkg/parser/schedule_fuzzy_scatter.go +++ b/pkg/parser/schedule_fuzzy_scatter.go @@ -90,25 +90,28 @@ func avoidHourBoundary(minute int) int { return minute } -// avoidPeakMinutes shifts minute values that fall within known high-traffic windows: +// avoidPeakMinutes shifts minute values that fall within 3 minutes of known high-traffic +// peak minutes during busy UTC hours: // -// - EU morning peak (06:00–09:59 UTC): avoids :30 minute (shifted to :31) -// - US business hours (14:00–18:59 UTC): avoids :15 and :45 minutes (shifted to :16 / :46) +// - EU morning peak (06:00–09:59 UTC): avoids minutes [27, 33] (±3 around :30), +// shifting any value in that window to 34 (first minute clearly outside the window) +// - US business hours (14:00–18:59 UTC): avoids minutes [12, 18] (±3 around :15) +// and [42, 48] (±3 around :45), shifting to 19 and 49 respectively // // All replacement values stay within [5, 54]. This is applied after avoidHourBoundary // for targeted-scatter patterns where the hour is determined by a user-specified target. func avoidPeakMinutes(hour, minute int) int { - // EU morning peak: avoid :30 in hours 06–09 - if hour >= 6 && hour <= 9 && minute == 30 { - return 31 + // EU morning peak: stay 3 minutes away from :30 in hours 06–09 + if hour >= 6 && hour <= 9 && minute >= 27 && minute <= 33 { + return 34 } - // US business hours (moderate): avoid :15 and :45 in hours 14–18 + // US business hours (moderate): stay 3 minutes away from :15 and :45 in hours 14–18 if hour >= 14 && hour <= 18 { - if minute == 15 { - return 16 + if minute >= 12 && minute <= 18 { + return 19 } - if minute == 45 { - return 46 + if minute >= 42 && minute <= 48 { + return 49 } } return minute diff --git a/pkg/parser/schedule_fuzzy_scatter_test.go b/pkg/parser/schedule_fuzzy_scatter_test.go index 05c4176240b..fe5d9f2586b 100644 --- a/pkg/parser/schedule_fuzzy_scatter_test.go +++ b/pkg/parser/schedule_fuzzy_scatter_test.go @@ -692,7 +692,8 @@ func TestScatterScheduleAvoidsHourBoundary(t *testing.T) { } // TestScatterScheduleAvoidsEUMorningPeak verifies that targeted-scatter patterns -// never produce minute :30 during EU morning peak hours (06:00–09:59 UTC). +// never produce a minute within 3 of :30 (i.e. minutes 27–33) during EU morning +// peak hours (06:00–09:59 UTC). func TestScatterScheduleAvoidsEUMorningPeak(t *testing.T) { workflowIDs := []string{ "workflow-a.md", "workflow-b.md", "workflow-c.md", @@ -736,9 +737,10 @@ func TestScatterScheduleAvoidsEUMorningPeak(t *testing.T) { return } - if hour >= 6 && hour <= 9 && minute == 30 { - t.Errorf("pattern=%q wfID=%q: cron %q schedules at :30 during EU morning peak (hours 06-09 UTC)", - pattern, wfID, result) + // Must stay 3 minutes away from :30 in hours 06-09 + if hour >= 6 && hour <= 9 && minute >= 27 && minute <= 33 { + t.Errorf("pattern=%q wfID=%q: cron %q schedules at :%02d during EU morning peak (must stay 3 min from :30 in hours 06-09 UTC)", + pattern, wfID, result, minute) } }) } @@ -746,7 +748,8 @@ func TestScatterScheduleAvoidsEUMorningPeak(t *testing.T) { } // TestScatterScheduleAvoidsUSBusinessHours verifies that targeted-scatter patterns -// never produce :15 or :45 minutes during US business hours (14:00–18:59 UTC). +// never produce a minute within 3 of :15 or :45 (i.e. [12,18] or [42,48]) during +// US business hours (14:00–18:59 UTC). func TestScatterScheduleAvoidsUSBusinessHours(t *testing.T) { workflowIDs := []string{ "workflow-a.md", "workflow-b.md", "workflow-c.md", @@ -791,9 +794,16 @@ func TestScatterScheduleAvoidsUSBusinessHours(t *testing.T) { return } - if hour >= 14 && hour <= 18 && (minute == 15 || minute == 45) { - t.Errorf("pattern=%q wfID=%q: cron %q schedules at :%02d during US business hours (hours 14-18 UTC)", - pattern, wfID, result, minute) + // Must stay 3 minutes away from :15 and :45 in hours 14-18 + if hour >= 14 && hour <= 18 { + if minute >= 12 && minute <= 18 { + t.Errorf("pattern=%q wfID=%q: cron %q schedules at :%02d during US business hours (must stay 3 min from :15 in hours 14-18 UTC)", + pattern, wfID, result, minute) + } + if minute >= 42 && minute <= 48 { + t.Errorf("pattern=%q wfID=%q: cron %q schedules at :%02d during US business hours (must stay 3 min from :45 in hours 14-18 UTC)", + pattern, wfID, result, minute) + } } }) }