From 45e69d122c0e146221f27f32e36cc81a0e525209 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 10 May 2026 09:15:02 +0800 Subject: [PATCH 1/2] Route monthly optimization tasks to Codex bridge --- .../workflows/auto_merge_optimization_pr.yml | 22 +++++- .github/workflows/auto_optimization_pr.yml | 4 +- README.md | 14 +++- docs/operator_runbook.md | 6 ++ scripts/fanout_monthly_optimization_tasks.py | 70 ++++++++++++++++--- scripts/prepare_auto_optimization_pr.py | 1 + ...st_auto_optimization_pr_workflow_config.py | 5 ++ .../test_fanout_monthly_optimization_tasks.py | 36 ++++++++++ 8 files changed, 143 insertions(+), 15 deletions(-) diff --git a/.github/workflows/auto_merge_optimization_pr.yml b/.github/workflows/auto_merge_optimization_pr.yml index 18d4620..f666e9a 100644 --- a/.github/workflows/auto_merge_optimization_pr.yml +++ b/.github/workflows/auto_merge_optimization_pr.yml @@ -7,7 +7,7 @@ name: Auto Merge Optimization PR jobs: auto-merge: - if: github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'automation/monthly-optimization-issue-') + if: github.event.workflow_run.conclusion == 'success' && (startsWith(github.event.workflow_run.head_branch, 'automation/monthly-optimization-issue-') || startsWith(github.event.workflow_run.head_branch, 'codex/monthly-optimization-issue-')) runs-on: ubuntu-latest permissions: contents: write @@ -36,8 +36,9 @@ jobs: if: steps.pr.outputs.pr_number != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: ${{ github.event.workflow_run.head_branch }} run: | - gh pr view "${{ steps.pr.outputs.pr_number }}" --json number,isDraft,body,url,files > data/output/auto_merge/pr.json + gh pr view "${{ steps.pr.outputs.pr_number }}" --json number,isDraft,body,url,files,labels > data/output/auto_merge/pr.json python3 - <<'PY' import json import os @@ -48,10 +49,20 @@ jobs: pr = json.loads(Path("data/output/auto_merge/pr.json").read_text(encoding="utf-8")) body = pr.get("body") or "" changed_files = [item.get("path", "") for item in pr.get("files", [])] + labels = {item.get("name", "") for item in pr.get("labels", [])} + branch_name = os.environ["BRANCH_NAME"] + codex_branch = branch_name.startswith("codex/monthly-optimization-issue-") guard = evaluate_changed_files(changed_files) has_marker = "` in the PR body, and mark the PR ready only after targeted tests pass. The post-CI `auto_merge_optimization_pr.yml` workflow can merge Codex PRs only when the PR is non-draft, carries `auto-merge-ok`, has the expected marker, reports task-level auto-merge eligibility, and touches no guarded selector/config paths. ## Dynamic Universe Logic diff --git a/docs/operator_runbook.md b/docs/operator_runbook.md index e16a4fa..bea850f 100644 --- a/docs/operator_runbook.md +++ b/docs/operator_runbook.md @@ -74,6 +74,12 @@ Boundary rules: - Research reports and shadow-track diagnostics stay upstream and are not part of the minimum downstream execution contract. - Telegram messages from this repository are operational release notifications, not trade execution alerts. +## Monthly Codex Remediation + +The monthly optimization planner may create repo-scoped follow-up issues after AI review. For `CryptoSnapshotPipelines`, low-risk non-experiment tasks marked `[auto-pr-safe]` are queued to the self-hosted VPS ccbot/Codex runner with the `codex-bridge` label. GitHub-hosted Claude Action remains only a manual-dispatch fallback; normal automated code remediation should run through ccbot/Codex. + +Codex remediation PRs must use branch `codex/monthly-optimization-issue-`, include `` in the PR body, and start as draft. The auto-merge workflow only merges after CI passes, the PR is ready for review, `auto-merge-ok` is present, task-level auto-merge eligibility is recorded, and changed files stay outside guarded selector/config paths. + ## Standard Monthly Flow 1. Refresh or verify local data: diff --git a/scripts/fanout_monthly_optimization_tasks.py b/scripts/fanout_monthly_optimization_tasks.py index de95b15..db99f82 100644 --- a/scripts/fanout_monthly_optimization_tasks.py +++ b/scripts/fanout_monthly_optimization_tasks.py @@ -16,6 +16,12 @@ LABEL_NAME = "monthly-optimization-task" LABEL_COLOR = "5319E7" LABEL_DESCRIPTION = "Automated repo-scoped monthly optimization tasks" +CODEX_LABEL_NAME = "codex-bridge" +CODEX_LABEL_COLOR = "0E8A16" +CODEX_LABEL_DESCRIPTION = "Queue this issue for the self-hosted ccbot Codex runner" +AUTO_MERGE_LABEL_NAME = "auto-merge-ok" +AUTO_MERGE_LABEL_COLOR = "0E8A16" +AUTO_MERGE_LABEL_DESCRIPTION = "Eligible for guarded automation merge after checks pass" RISK_ORDER = {"low": 0, "medium": 1, "high": 2} @@ -44,6 +50,24 @@ def _repo_actions(plan: dict[str, Any], owner_repo: str) -> list[dict[str, Any]] return list(repo_summary.get("actions", [])) +def _has_codex_bridge_actions(actions: list[dict[str, Any]], owner_repo: str) -> bool: + if owner_repo != "CryptoSnapshotPipelines": + return False + return any( + action.get("risk_level") == "low" + and action.get("auto_pr_safe") + and not action.get("experiment_only") + for action in actions + ) + + +def issue_labels_for_actions(actions: list[dict[str, Any]], owner_repo: str) -> list[str]: + labels = [LABEL_NAME] + if _has_codex_bridge_actions(actions, owner_repo): + labels.extend([CODEX_LABEL_NAME, AUTO_MERGE_LABEL_NAME]) + return labels + + def build_issue_body(plan: dict[str, Any], owner_repo: str, planner_issue_url: str | None = None) -> str: actions = _repo_actions(plan, owner_repo) repo_summary = plan.get("repo_action_summary", {}).get(owner_repo, {}) @@ -61,6 +85,23 @@ def build_issue_body(plan: dict[str, Any], owner_repo: str, planner_issue_url: s if planner_issue_url: lines.append(f"- Planner issue: {planner_issue_url}") + if _has_codex_bridge_actions(actions, owner_repo): + lines.extend( + [ + "", + "## Codex Bridge Contract", + "", + f"- Queue label: `{CODEX_LABEL_NAME}`", + "- Runner: self-hosted VPS ccbot/Codex, not GitHub-hosted Claude Action.", + "- Implement only `low` actions explicitly marked `[auto-pr-safe]` and not `[experiment-only]`.", + "- Keep edits minimal and limited to docs, report wording, validation, workflow plumbing, instrumentation, and tests.", + "- Do not change production selector logic, ranking behavior, `src/`, or `config/` from this issue alone.", + "- Open a draft PR first from branch `codex/monthly-optimization-issue-`.", + "- Include `` in the PR body.", + "- Mark the PR ready and apply `auto-merge-ok` only after targeted tests pass and changed files stay inside the guardrails.", + ] + ) + lines.extend(["", "## Actions"]) for action in actions: flags: list[str] = [] @@ -111,8 +152,8 @@ def github_request(method: str, url: str, token: str, payload: dict[str, Any] | return json.loads(raw) if raw else None -def ensure_label(api_url: str, repo: str, token: str) -> None: - label_path = urllib.parse.quote(LABEL_NAME, safe="") +def ensure_label(api_url: str, repo: str, token: str, *, name: str, color: str, description: str) -> None: + label_path = urllib.parse.quote(name, safe="") label_url = f"{api_url}/repos/{repo}/labels/{label_path}" try: github_request("GET", label_url, token) @@ -124,17 +165,28 @@ def ensure_label(api_url: str, repo: str, token: str) -> None: f"{api_url}/repos/{repo}/labels", token, { - "name": LABEL_NAME, - "color": LABEL_COLOR, - "description": LABEL_DESCRIPTION, + "name": name, + "color": color, + "description": description, }, ) -def upsert_issue(*, api_url: str, repo: str, token: str, title: str, body: str) -> tuple[str, int, str]: +def ensure_labels(api_url: str, repo: str, token: str, labels: list[str]) -> None: + specs = { + LABEL_NAME: (LABEL_COLOR, LABEL_DESCRIPTION), + CODEX_LABEL_NAME: (CODEX_LABEL_COLOR, CODEX_LABEL_DESCRIPTION), + AUTO_MERGE_LABEL_NAME: (AUTO_MERGE_LABEL_COLOR, AUTO_MERGE_LABEL_DESCRIPTION), + } + for label in labels: + color, description = specs[label] + ensure_label(api_url, repo, token, name=label, color=color, description=description) + + +def upsert_issue(*, api_url: str, repo: str, token: str, title: str, body: str, labels: list[str]) -> tuple[str, int, str]: marker = build_marker_from_body(body) existing = find_existing_issue(api_url=api_url, repo=repo, token=token, marker=marker) - payload = {"title": title, "body": body, "labels": [LABEL_NAME]} + payload = {"title": title, "body": body, "labels": labels} if existing: github_request("PATCH", f"{api_url}/repos/{repo}/issues/{existing['number']}", token, payload) return "updated", int(existing["number"]), str(existing["html_url"]) @@ -264,15 +316,17 @@ def main() -> int: title = build_issue_title(plan, args.owner_repo) body = build_issue_body(plan, args.owner_repo, planner_issue_url=args.planner_issue_url) + labels = issue_labels_for_actions(actions, args.owner_repo) try: - ensure_label(args.api_url.rstrip("/"), args.repo, token) + ensure_labels(args.api_url.rstrip("/"), args.repo, token, labels) status, issue_number, issue_url = upsert_issue( api_url=args.api_url.rstrip("/"), repo=args.repo, token=token, title=title, body=body, + labels=labels, ) result = build_result( owner_repo=args.owner_repo, diff --git a/scripts/prepare_auto_optimization_pr.py b/scripts/prepare_auto_optimization_pr.py index e3f3535..fe3b6a8 100644 --- a/scripts/prepare_auto_optimization_pr.py +++ b/scripts/prepare_auto_optimization_pr.py @@ -108,6 +108,7 @@ REPO_NAME_ALIASES = { "CryptoLeaderRotation": "CryptoSnapshotPipelines", + "crypto-codex-bridge": "CryptoSnapshotPipelines", } diff --git a/tests/test_auto_optimization_pr_workflow_config.py b/tests/test_auto_optimization_pr_workflow_config.py index 2046229..7c63f71 100644 --- a/tests/test_auto_optimization_pr_workflow_config.py +++ b/tests/test_auto_optimization_pr_workflow_config.py @@ -16,6 +16,7 @@ def test_auto_optimization_workflow_handles_monthly_task_issues(self) -> None: self.assertIn("issues:", workflow) self.assertIn("monthly-optimization-task", workflow) + self.assertIn("!contains(github.event.issue.labels.*.name, 'codex-bridge')", workflow) self.assertIn("workflow_dispatch:", workflow) self.assertIn("issue_number:", workflow) self.assertIn("actions: write", workflow) @@ -46,9 +47,13 @@ def test_auto_merge_workflow_waits_for_ci_and_merges_only_safe_ready_prs(self) - self.assertIn("workflow_run:", workflow) self.assertIn('workflows: ["CI"]', workflow) self.assertIn("automation/monthly-optimization-issue-", workflow) + self.assertIn("codex/monthly-optimization-issue-", workflow) self.assertIn("gh pr view", workflow) + self.assertIn("labels", workflow) self.assertIn("evaluate_changed_files", workflow) self.assertIn("Task-level auto-merge eligible: `yes`", workflow) + self.assertIn("auto-merge-ok", workflow) + self.assertIn("missing_auto_merge_label", workflow) self.assertIn("gh pr merge", workflow) def test_ci_workflow_supports_manual_dispatch(self) -> None: diff --git a/tests/test_fanout_monthly_optimization_tasks.py b/tests/test_fanout_monthly_optimization_tasks.py index 322bb57..c8c716f 100644 --- a/tests/test_fanout_monthly_optimization_tasks.py +++ b/tests/test_fanout_monthly_optimization_tasks.py @@ -7,6 +7,7 @@ build_issue_body, build_issue_title, build_marker, + issue_labels_for_actions, ) @@ -89,6 +90,41 @@ def test_build_closed_issue_body_marks_repo_as_resolved(self) -> None: self.assertIn("No repo-scoped tasks remain", body) self.assertIn("This issue is being closed", body) + def test_crypto_snapshot_low_risk_tasks_are_queued_for_codex_bridge(self) -> None: + crypto_plan = { + **self.plan, + "repo_action_summary": { + "CryptoSnapshotPipelines": { + "count": 1, + "highest_risk_level": "low", + "actions": [ + { + "risk_level": "low", + "title": "Improve monthly review diagnostics", + "summary": "Keep release warnings visible in the report.", + "source_repo": "QuantStrategyLab/CryptoSnapshotPipelines", + "source_issue_number": 11, + "source_issue_url": "https://github.com/QuantStrategyLab/CryptoSnapshotPipelines/issues/11", + "auto_pr_safe": True, + "experiment_only": False, + } + ], + } + }, + } + + actions = crypto_plan["repo_action_summary"]["CryptoSnapshotPipelines"]["actions"] + body = build_issue_body(crypto_plan, "CryptoSnapshotPipelines") + + self.assertEqual( + issue_labels_for_actions(actions, "CryptoSnapshotPipelines"), + ["monthly-optimization-task", "codex-bridge", "auto-merge-ok"], + ) + self.assertIn("## Codex Bridge Contract", body) + self.assertIn("self-hosted VPS ccbot/Codex", body) + self.assertIn("codex/monthly-optimization-issue-", body) + self.assertIn("", body) + if __name__ == "__main__": unittest.main() From ed88f8da6eab7144f409e452554796b3dd472b85 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 10 May 2026 10:01:02 +0800 Subject: [PATCH 2/2] Requeue Codex PR feedback to bridge issues --- .github/workflows/codex_pr_feedback.yml | 128 ++++++++++++++++++ README.md | 2 + docs/operator_runbook.md | 2 + ...st_auto_optimization_pr_workflow_config.py | 12 ++ 4 files changed, 144 insertions(+) create mode 100644 .github/workflows/codex_pr_feedback.yml diff --git a/.github/workflows/codex_pr_feedback.yml b/.github/workflows/codex_pr_feedback.yml new file mode 100644 index 0000000..03bbbf8 --- /dev/null +++ b/.github/workflows/codex_pr_feedback.yml @@ -0,0 +1,128 @@ +name: Codex PR Feedback + +"on": + workflow_run: + workflows: ["CI"] + types: [completed] + pull_request_review: + types: [submitted] + +jobs: + ci-feedback: + if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' && startsWith(github.event.workflow_run.head_branch, 'codex/monthly-optimization-issue-') + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read + + steps: + - name: Post CI failure back to Codex issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: ${{ github.event.workflow_run.head_branch }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + RUN_NAME: ${{ github.event.workflow_run.name }} + run: | + mkdir -p data/output/codex_feedback + gh pr list --state open --head "${BRANCH_NAME}" --json number,title,url,body > data/output/codex_feedback/pr.json + python3 - <<'PY' + import json + import os + import re + import textwrap + from pathlib import Path + + prs = json.loads(Path("data/output/codex_feedback/pr.json").read_text(encoding="utf-8")) + if not prs: + Path("data/output/codex_feedback/skip.txt").write_text("No open Codex PR found.\n", encoding="utf-8") + raise SystemExit(0) + + pr = prs[0] + body = pr.get("body") or "" + match = re.search(r"", body) + if not match: + Path("data/output/codex_feedback/skip.txt").write_text("No source issue marker found.\n", encoding="utf-8") + raise SystemExit(0) + + issue_number = match.group(1) + comment = textwrap.dedent( + f"""\ + + ## Codex PR CI Feedback + + CI failed for the Codex remediation PR. + + - PR: {pr['url']} + - Branch: `{os.environ['BRANCH_NAME']}` + - Workflow: `{os.environ['RUN_NAME']}` + - Run: {os.environ['RUN_URL']} + + Codex should inspect the failing GitHub Actions logs, update the same PR branch, run targeted tests, and keep the PR draft until the fix is verified. + """ + ) + Path("data/output/codex_feedback/issue_number.txt").write_text(issue_number, encoding="utf-8") + Path("data/output/codex_feedback/comment.md").write_text(comment.strip() + "\n", encoding="utf-8") + PY + if [ -f data/output/codex_feedback/issue_number.txt ]; then + gh issue comment "$(cat data/output/codex_feedback/issue_number.txt)" --body-file data/output/codex_feedback/comment.md + else + cat data/output/codex_feedback/skip.txt >> "$GITHUB_STEP_SUMMARY" + fi + + review-feedback: + if: github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested' && startsWith(github.event.pull_request.head.ref, 'codex/monthly-optimization-issue-') + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - name: Post review feedback back to Codex issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_BODY: ${{ github.event.pull_request.body }} + REVIEW_URL: ${{ github.event.review.html_url }} + REVIEW_AUTHOR: ${{ github.event.review.user.login }} + REVIEW_BODY: ${{ github.event.review.body }} + run: | + mkdir -p data/output/codex_feedback + python3 - <<'PY' + import os + import re + import textwrap + from pathlib import Path + + body = os.environ.get("PR_BODY") or "" + match = re.search(r"", body) + if not match: + Path("data/output/codex_feedback/skip.txt").write_text("No source issue marker found.\n", encoding="utf-8") + raise SystemExit(0) + + review_body = (os.environ.get("REVIEW_BODY") or "_No review body supplied._").strip() + comment = textwrap.dedent( + f"""\ + + ## Codex PR Review Feedback + + A review requested changes on the Codex remediation PR. + + - PR: {os.environ['PR_URL']} + - Reviewer: @{os.environ['REVIEW_AUTHOR']} + - Review: {os.environ['REVIEW_URL']} + + ### Review Body + + {review_body} + + Codex should update the same PR branch, address the requested changes, run targeted tests, and leave the PR draft until the fix is verified. + """ + ) + Path("data/output/codex_feedback/issue_number.txt").write_text(match.group(1), encoding="utf-8") + Path("data/output/codex_feedback/comment.md").write_text(comment.strip() + "\n", encoding="utf-8") + PY + if [ -f data/output/codex_feedback/issue_number.txt ]; then + gh issue comment "$(cat data/output/codex_feedback/issue_number.txt)" --body-file data/output/codex_feedback/comment.md + else + cat data/output/codex_feedback/skip.txt >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/README.md b/README.md index f4b4740..fb51c44 100644 --- a/README.md +++ b/README.md @@ -569,6 +569,8 @@ The monthly optimization planner creates repo-scoped issues for follow-up tasks. The `codex-bridge` label is consumed by the self-hosted VPS ccbot/Codex runner. Codex should open a draft PR from `codex/monthly-optimization-issue-`, include `` in the PR body, and mark the PR ready only after targeted tests pass. The post-CI `auto_merge_optimization_pr.yml` workflow can merge Codex PRs only when the PR is non-draft, carries `auto-merge-ok`, has the expected marker, reports task-level auto-merge eligibility, and touches no guarded selector/config paths. +If a Codex remediation PR fails CI or receives a changes-requested review, `codex_pr_feedback.yml` comments the failure or review summary back to the source `codex-bridge` issue. Because the VPS bridge re-dispatches updated issues, Codex can fix the same PR branch without manual handoff. + ## Dynamic Universe Logic The universe is a hard filter layer, not the final holdings set. diff --git a/docs/operator_runbook.md b/docs/operator_runbook.md index bea850f..9551560 100644 --- a/docs/operator_runbook.md +++ b/docs/operator_runbook.md @@ -80,6 +80,8 @@ The monthly optimization planner may create repo-scoped follow-up issues after A Codex remediation PRs must use branch `codex/monthly-optimization-issue-`, include `` in the PR body, and start as draft. The auto-merge workflow only merges after CI passes, the PR is ready for review, `auto-merge-ok` is present, task-level auto-merge eligibility is recorded, and changed files stay outside guarded selector/config paths. +If CI fails on a Codex remediation PR, or a reviewer requests changes, `Codex PR Feedback` comments the failure or review summary back to the source `codex-bridge` issue. The issue update lets the VPS bridge dispatch Codex again to fix the same PR branch. + ## Standard Monthly Flow 1. Refresh or verify local data: diff --git a/tests/test_auto_optimization_pr_workflow_config.py b/tests/test_auto_optimization_pr_workflow_config.py index 7c63f71..f72701c 100644 --- a/tests/test_auto_optimization_pr_workflow_config.py +++ b/tests/test_auto_optimization_pr_workflow_config.py @@ -7,6 +7,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] AUTO_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "auto_optimization_pr.yml" MERGE_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "auto_merge_optimization_pr.yml" +FEEDBACK_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "codex_pr_feedback.yml" CI_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "ci.yml" @@ -60,6 +61,17 @@ def test_ci_workflow_supports_manual_dispatch(self) -> None: workflow = CI_WORKFLOW.read_text(encoding="utf-8") self.assertIn("workflow_dispatch:", workflow) + def test_codex_feedback_workflow_requeues_failed_ci_and_review_feedback(self) -> None: + workflow = FEEDBACK_WORKFLOW.read_text(encoding="utf-8") + + self.assertIn("workflow_run:", workflow) + self.assertIn("pull_request_review:", workflow) + self.assertIn("codex/monthly-optimization-issue-", workflow) + self.assertIn("auto-optimization-pr:issue-", workflow) + self.assertIn("gh issue comment", workflow) + self.assertIn("Codex PR CI Feedback", workflow) + self.assertIn("Codex PR Review Feedback", workflow) + if __name__ == "__main__": unittest.main()