Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .github/workflows/auto_merge_optimization_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 = "<!-- auto-optimization-pr:issue-" in body
task_level_allowed = "Task-level auto-merge eligible: `yes`" in body
should_merge = has_marker and task_level_allowed and not pr.get("isDraft") and guard["allowed"]
has_auto_merge_label = "auto-merge-ok" in labels
should_merge = (
has_marker
and task_level_allowed
and not pr.get("isDraft")
and guard["allowed"]
and (not codex_branch or has_auto_merge_label)
Comment on lines +61 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove undocumented task-level gate for Codex PR merges

The Codex branch path is now routed through this gate, but should_merge still requires task_level_allowed from a literal PR-body string ("Task-level auto-merge eligible: yes"). The new Codex contract text generated in scripts/fanout_monthly_optimization_tasks.py does not instruct Codex to include that marker, so a Codex PR can satisfy the documented branch/marker/label rules and still be permanently skipped with task_level_guard. This effectively blocks the new Codex auto-merge flow unless out-of-band behavior adds that exact line.

Useful? React with 👍 / 👎.

)
if not has_marker:
reason = "missing_marker"
elif not task_level_allowed:
Expand All @@ -60,14 +71,19 @@ jobs:
reason = "draft_pr"
elif not guard["allowed"]:
reason = "sensitive_changed_files"
elif codex_branch and not has_auto_merge_label:
reason = "missing_auto_merge_label"
else:
reason = "ready"

summary_lines = [
"## Auto-Merge Gate",
f"- PR: {pr['url']}",
f"- Branch: `{branch_name}`",
f"- Codex branch: `{ 'yes' if codex_branch else 'no' }`",
f"- Draft: `{ 'yes' if pr.get('isDraft') else 'no' }`",
f"- Task-level auto-merge eligible: `{ 'yes' if task_level_allowed else 'no' }`",
f"- Auto-merge label: `{ 'yes' if has_auto_merge_label else 'no' }`",
f"- Sensitive files touched: `{len(guard['blocked_files'])}`",
f"- Final merge decision: `{ 'merge' if should_merge else 'skip' }`",
f"- Reason: `{reason}`",
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/auto_optimization_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ name: Auto Optimization Draft PR

jobs:
auto-pr:
if: contains(github.event.issue.labels.*.name, 'monthly-optimization-task') || inputs.issue_number != ''
if: inputs.issue_number != '' || (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'monthly-optimization-task') && !contains(github.event.issue.labels.*.name, 'codex-bridge'))
runs-on: ubuntu-latest
permissions:
actions: write
Expand All @@ -36,7 +36,7 @@ jobs:
mkdir -p data/output/auto_optimization
if [ -z "${ANTHROPIC_API_KEY}" ]; then
echo "has_anthropic_key=false" >> "$GITHUB_OUTPUT"
echo "Claude automation skipped: ANTHROPIC_API_KEY is not configured for this repo." > data/output/auto_optimization/skip_reason.txt
echo "Legacy Claude automation skipped: ANTHROPIC_API_KEY is not configured for this repo." > data/output/auto_optimization/skip_reason.txt
else
echo "has_anthropic_key=true" >> "$GITHUB_OUTPUT"
fi
Expand Down
128 changes: 128 additions & 0 deletions .github/workflows/codex_pr_feedback.yml
Original file line number Diff line number Diff line change
@@ -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"<!--\s*auto-optimization-pr:issue-(\d+)\s*-->", 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-feedback:ci:{pr['number']} -->
## 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"<!--\s*auto-optimization-pr:issue-(\d+)\s*-->", 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-feedback:review:{os.environ['PR_NUMBER']} -->
## 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
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ The AI review covers:
- **Anomaly detection**: flags unexpected warnings, stale artifacts, validation failures, or suspicious ranking scores
- **Downstream impact**: notes implications for BinancePlatform (the downstream execution engine), including pool changes and degradation risk
- **Operator action items**: summarizes the checklist and adds any AI-identified follow-up items
- **Code improvements**: structured review output can feed the monthly optimization planner; low-risk `auto-pr-safe` tasks may become automation PRs, while sensitive selector changes remain manual-review work
- **Code improvements**: structured review output feeds the monthly optimization planner; concrete low-risk `auto-pr-safe` tasks for `CryptoSnapshotPipelines` are queued to the self-hosted ccbot/Codex runner with `codex-bridge`, while sensitive selector changes remain manual-review work

All analysis is posted in both English and Chinese.

Expand All @@ -557,7 +557,19 @@ gh secret set ANTHROPIC_API_KEY --body "sk-ant-..."
gh secret set OPENAI_API_KEY --body "sk-..."
```

The AI review workflow runs on `ubuntu-latest` (no self-hosted runner required) and costs approximately $0.01-0.05 per monthly run.
The AI review workflow runs on `ubuntu-latest` (no self-hosted runner required) and costs approximately $0.01-0.05 per monthly run. Code remediation is a separate phase: repo-scoped low-risk tasks are created as GitHub issues, and safe `CryptoSnapshotPipelines` tasks are handed to the VPS ccbot/Codex bridge instead of GitHub-hosted Claude Action.

### Codex Remediation and Auto-Merge Gate

The monthly optimization planner creates repo-scoped issues for follow-up tasks. For this repository, low-risk non-experiment tasks marked `[auto-pr-safe]` are labeled with:

- `monthly-optimization-task`
- `codex-bridge`
- `auto-merge-ok`

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-<issue-number>`, include `<!-- auto-optimization-pr:issue-<issue-number> -->` 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

Expand Down
8 changes: 8 additions & 0 deletions docs/operator_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ 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-<issue-number>`, include `<!-- auto-optimization-pr:issue-<issue-number> -->` 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:
Expand Down
70 changes: 62 additions & 8 deletions scripts/fanout_monthly_optimization_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}


Expand Down Expand Up @@ -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, {})
Expand All @@ -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-<issue-number>`.",
"- Include `<!-- auto-optimization-pr:issue-<issue-number> -->` 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] = []
Expand Down Expand Up @@ -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)
Expand All @@ -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"])
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions scripts/prepare_auto_optimization_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@

REPO_NAME_ALIASES = {
"CryptoLeaderRotation": "CryptoSnapshotPipelines",
"crypto-codex-bridge": "CryptoSnapshotPipelines",
}


Expand Down
Loading