Skip to content

ci: let a merge queue qualify candidates on both required checks - #5297

Merged
huangruiteng merged 4 commits into
loopx-project:mainfrom
songoow:codex/ci-merge-queue-ready
Sep 30, 2026
Merged

huangruiteng merged 4 commits into
loopx-project:mainfrom
songoow:codex/ci-merge-queue-ready

Conversation

@songoow

@songoow songoow commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

The protect main ruleset sets strict_required_status_checks_policy: true, and the required checks are Sign-off and merge-gate. Every merge to main therefore makes every other open PR out of date. Each of those PRs has to merge main (locally, to keep DCO sign-off) and rerun the full Python Tests qualification: a median of about 45 minutes, and about 69 minutes at p90, over recent PR runs. With several dozen open PRs, most CI capacity goes to these reruns, and the extra jobs also add queueing to everyone else's runs.

A GitHub merge queue removes that multiplier: a PR is qualified on its own pushes, and the queue qualifies the exact merged candidate once. The queue can only be enabled if both required checks report on merge_group events. Today neither workflow does, so enabling a queue would stall every entry.

Change

  • python-tests.yml adds on: merge_group. The changes job already sends any non-pull_request event through review_gate.py classify --non-pr, which always plans full. Queue candidates therefore get no exemption, exactly like main.
  • sonar is skipped for merge_group, so temporary gh-readonly-queue/* refs do not create SonarCloud branches.
  • node-forward-compatibility changes from event_name != 'pull_request' to push || workflow_dispatch. The 45-minute non-blocking probe stays off the queue, and its behavior on push and dispatch is unchanged.
  • dco.yml adds on: merge_group. It reads merge_group.base_ref and merge_group.head_sha when there is no pull-request payload. It strips the refs/heads/ prefix that the queue event carries, and fails closed if either coordinate is empty. The contribution-range logic and the verified-GitHub-merge exemption are unchanged.

Default-off: until the ruleset enables a merge queue, GitHub never emits merge_group. pull_request behavior in both workflows is unchanged. For push and workflow_dispatch, the only difference is the forward-probe condition, which was already true for those events.

Activation (maintainer ruleset decision, not part of this PR)

  1. Add a merge-queue rule to protect main with merge method merge. Sign-off exempts only verified GitHub-generated two-parent merges; a squash or rebase queue would produce commits it does not recognize, and the check would fail closed.
  2. After the first queue entries pass, relax "require branches to be up to date" (strict_required_status_checks_policy). That is where the rerun savings come from.

The first real queue run is the first live observation of GitHub's queue-commit provenance. If Sign-off rejects the queue merge commit, the queue fails loudly rather than silently passing.

Validation

  • pytest tests/test_dco_workflow.py: 24 passed. The test harness now evaluates the workflow's actual ${{ a || b }} env expressions for each event type. New cases cover a merge-queue candidate: signed passes, unsigned fails, a verified platform merge is exempt only with provenance, and a missing base ref or head fails closed. Mutation check: removing the refs/heads/ normalization fails 3 of the new tests.
  • pytest tests/test_python_ci_workflow.py: 98 passed. The new test checks that merge_group is a trigger, that only pull_request can take the exemption-classifying branch, that Sonar skips queue refs and that merge-gate still always reports. impact_plan.plan(..., pull_request=False) returns full.
  • loopx canary premerge --from-git-diff (tier standard): 13 selected, 13 executed, 0 failures; public-boundary scan clean. self_merge_allowed: false. This changes required-check workflows, so it is left for maintainer merge.
  • Untested: a live merge_group event. That needs the ruleset change above.

Future-facing pass

Once the queue is active, a possible follow-up is to run some lanes (for example, Windows and Stage 2C) only in the queue rather than on every PR push. That would change the "PRs get full qualification" policy, so it is not proposed here and needs an explicit decision.

Independent of #5295, which moves browser qualification off the critical path. Both touch python-tests.yml, in different sections.

🤖 Generated with Claude Code

The main ruleset requires an up-to-date branch. Every merge therefore
forces each open PR to merge `main` and rerun the full ~45 minute
qualification. A GitHub merge queue removes that multiplier, but only if
both required checks report on `merge_group` events.

- `python-tests.yml` runs on `merge_group`. Queue candidates take the
  non-PR classification path, which always plans full qualification.
  Sonar skips temporary queue refs, and the Node forward probe stays on
  push/dispatch.
- `dco.yml` runs on `merge_group`, reads the queue's base ref and head
  SHA, normalizes the full `refs/heads/` base ref, and fails closed when
  either coordinate is missing.

The trigger stays inert until the ruleset enables a merge queue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
@kokokoXUY

Copy link
Copy Markdown
Contributor

Independent verification of the queue-candidate path, plus one verified nit.

1. The central claim checks out. scripts/ci/impact_plan.py:29-31 short-circuits before it
looks at the diff:

def candidate(changes, *, pull_request: bool = True):
    if not pull_request:
        return "full", "main and manual runs retain full qualification"

so a merge_group event (which takes the else branch in the changes job, github.event_name != 'pull_request') always plans full, and job_flags("full") (line 49) sets
core_tests/python_tests/stage2c_tests = True, presentation_tests = False -- i.e. exactly the
push-to-main plan. The comment you added to test_merge_queue_candidates_run_full_qualification
is accurate.

2. Every gating if: in python-tests.yml was audited. I enumerated each job if: that
mentions event_name. Only two exclude a queue run -- node-forward-compatibility (the
non-blocking probe, now push || workflow_dispatch) and sonar (!= 'merge_group') -- both
intentional. merge-gate is if: always(), so the required check does report on the candidate, and
every other job gates on needs.changes.outputs.*, which is full for a queue run. No lane is
silently skipped while the gate still reports.

3. dco.yml needs no pull-request metadata. The step consumes only BASE_REF, HEAD_SHA and
GITHUB_REPOSITORY; there is no github.event.pull_request.number and no PR API call anywhere in
the file, so the || fallback is sufficient on merge_group. BASE_REF="${BASE_REF#refs/heads/}"
is a no-op for the short pull_request ref and correct for refs/heads/main.

4. Nit (verified, non-blocking): the assertion on dco.yml:32 is not load-bearing.

test -n "${BASE_REF}" && test -n "${HEAD_SHA}"

does not fail the step under set -euo pipefail. Commands in an AND-OR list other than the last
are exempt from errexit; I confirmed in bash that
set -euo pipefail; A=""; test -n "$A" && test -n "x"; echo REACHED prints REACHED and exits 0.
The range lookup does not cover for it either: git rev-list --reverse "<base>.." with an empty
right-hand side is parsed as <base>..HEAD (in a scratch 4-commit repo A.. and A..HEAD both
list the same 2 commits) and exits 0, so an empty HEAD_SHA would silently check the checked-out
HEAD range instead of erroring -- the opposite of what the comment above the lookup asks for.

Your new test_missing_event_coordinates_fail_closed passes for a different reason than the guard:
with base_ref="release/next" the fixture has no such remote branch, so the git fetch on line 36
is what exits non-zero. The guard itself is never exercised.

If you want that assertion to be real, make it a statement errexit can see, e.g.

if [[ -z "${BASE_REF}" || -z "${HEAD_SHA}" ]]; then
  echo "::error::missing base ref or head sha for ${GITHUB_EVENT_NAME}"
  exit 1
fi

Impact today is low -- both coordinates are always present on pull_request and merge_group --
so this is not a merge blocker, just the one place the file relies on a check that does not do what
its comment says.

Local run caveat (host, not your diff). I applied your head and ran both modified test files in
a clean checkout on Windows. 22 tests in them already fail on main here (the bash-capturing
harness hits a GBK decode / TypeError), and your three new tests fail for the identical reason --
their pull_request counterparts are in that same 22. So this run is not a green signal either way;
CI remains the authority. The plan/classifier logic above I verified by reading the code and by
enumerating the workflow conditions, not from that run.

Pick up the main fixes from loopx-project#5288 and loopx-project#5292.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
huangruiteng
huangruiteng previously approved these changes Sep 29, 2026

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

动机

按 LoopX PR review capability 的 policy revision 12 评审完整 head 0833b9202142353b35723b776911cec572aa6560,基线固定为 5ab23b3f67a2c1098717ebb450572c8158683153。没有阻断性发现,有一项输入诊断的非阻断 refinement。旧工作流不接收 merge-group 事件,即使规则集启用队列也无法给队列候选报告两个必需检查;本 PR 补齐这个入口,是 S12 贡献/合并体验的准备阶段,不是宣称队列已启用或已测得端到端提速。

改动思路

让 Sign-off 和 merge-gate 沿用既有规则检查队列候选,而不是用 PR head 的旧结果代替候选结果。GitHub 官方事件契约 明确区分 merge_group 与 PR/push;必需 workflow 需要额外订阅这个事件。本 PR 只扩展 workflow 入口:是否启用队列、变更规则集或放松 up-to-date 要求,仍由维护者作独立决定。

普通贡献者目前仍走原来的提交、检查失败、修复/重跑路径,不增加必填配置或新权限。启用后的预期路径才是入队 → 完整候选资格验证 → 两个必需结果;失败交还原有 CI/DCO 修复 owner。节点前向探测和 Sonar 不属于这两个必需结果,不应额外阻塞队列。该阶段可独立验证和回滚,真正的 GitHub 队列触发、运行时 provenance 与运行耗时仍需维护者启用后观察。

具体改动

关键代码讲解

  • .github/workflows/dco.yml:3–6,24–40 添加 merge_group,通过原 PR 坐标或 queue base_ref/head_sha 取得检查范围;去掉 refs/heads/ 前缀后仍刷新精确目标分支,再用真实 git rev-list 查贡献提交。基线已有的“较新上游未签署提交不归该 PR 修”的规则保留。
  • dco.yml:47–72 的 DCO/平台合并判断没有放宽。贡献提交仍需 trailer;无 trailer 的两父 GitHub integration commit 只有 exact SHA、签名验证、web-flow 身份及有序父节点全部匹配才可走既有例外,其父贡献独立检查。伪造显示名和 API 不可用不能变成成功。
  • python-tests.yml:56–83 的分类入口未另造 queue classifier:非 PR 分支调用原 review_gate.py classify --non-pr,得到 full,不会借 docs-only 或 presentation-only diff 获得队列豁免;最终 merge-gate 仍 always() 执行原 verify()。
  • :364–369 将非阻断 Node 26 probe 精确限定为 push/dispatch;Sonar 上传只在非 merge-group 事件执行。PR、push、manual 原路径条件保持;docs、presentation 与 full 的原分类契约不变。文档同步声明必须使用 merge 方法及维护者规则集边界。

正向执行:合成 merge-group 携带完整 base ref → 正规化 → refresh 实际 Git remote → 范围内签署贡献通过;非 PR 分类实际写出 full plan → 原 CLI gate 接受全部应成功的 lane。负向执行:未签署贡献、缺失坐标、无法解析的 ref、API/签名/父节点不匹配或应成功 lane 被跳过都拒绝。base/head 使用相同不可变 Git 输入,普通 PR 的签署/未签署结果、上游刷新和完整诊断一致;原分类/merge-gate 的 PR、push、dispatch 三个入口也保持一致。

本地验证:

uv run --extra test python -m pytest -q tests/test_python_ci_workflow.py tests/test_dco_workflow.py
uv run --extra test python -m unittest discover -s scripts/ci -p 'test_*.py'
uv run --extra test python examples/github-actions-runtime-smoke.py
uv run --extra test python -m ruff check tests/test_dco_workflow.py tests/test_python_ci_workflow.py
uv run --extra test loopx --format json canary premerge --from-git-diff --git-diff-base 5ab23b3f67a2c1098717ebb450572c8158683153 --tier standard --no-progress

122 pytest、7 CI 单测、runtime smoke、Ruff、4 个 canary 直接检查与 13 个选中检查全部通过,基线对应 pytest 为 117 项。额外探针执行了真实 workflow Bash、真实本地 bare Git remote 及原 classify/verify CLI;签署贡献的 full-ref 正向对照成功,删除 ref 正规化则失败;重新排序 needs 不改变结果,矛盾豁免和非预期 skip 被拒绝。平台 commit metadata 的负向矩阵使用替代 API 返回值,不能视为已从线上队列获取 provenance;没有修改活动规则集或启动队列。

非阻断 refinement:空 base 的 guard 应在输入边界退出

位置:dco.yml:32 的 test -n "$BASE_REF" && test -n "$HEAD_SHA"。在 set -e 下,左侧为空不会在那里退出,随后 Git 以 invalid refspec 拒绝(实际 exit 128);valid base + 空 head 则在右侧退出(实际 exit 1)。因此当前路径没有复现“空 head 被当成 HEAD 并误通过”的风险,也不是放宽 DCO 的 blocker。但空 base 的错误归因不够直接。

最小修整是拆成两个独立检查,分别输出缺少 base/head 的明确错误再退出;不需要重写 DCO owner。回归应提供真实存在的目标分支,并分别覆盖空 base、空 head、两者都空,断言在调用 fetch/rev-list 前拒绝。现有 fixture 已创建 release/next;额外探针也使用真实分支和未签署候选确认拒绝,而非靠“不存在的分支”得到偶然失败。这个建议不影响本次 APPROVE。

语义与 CI 对齐

必需检查从仓库的 docs/development/testing-and-quality.md 解析为 Sign-off/merge-gate,不是从远端临时 check 名猜测。新增的是 GitHub 外部事件坐标,复用 loopx_ci_job_plan_v1 和既有 DCO 验证语义;没有新的 actor 生命周期、写权限、Goal 状态或手动同步标志。默认关闭的是规则集队列,不是发现 trigger 就自动激活。关停队列时 PR/push/dispatch 的条件、classifier、门禁和提示保持原行为;启用时队列按既有 non-PR full 分支处理。

对主干的风险

最强风险是误把队列候选当成可豁免的普通 PR,或把未签署贡献当作平台 merge 放过。实际 CLI 的完整规划、失败/跳过拒绝矩阵及既有 provenance 负向测试覆盖这些分支;API 故障明确 fail closed,恢复 owner 是 CI 重跑/贡献者 DCO 修复,绝不是 bypass 来伪造通过。工作流没有新增 secrets 或写权限,规则集 activation 留在维护者边界;没有产品前端/Lark/持久化变化,无需配套 UI。

本机 Bash 3.2 在未变的 PR 分类 shell 里报 extra[@]: unbound variable,同输入基线/本 head 的完整错误一致;独立执行实际分类 CLI、非 PR shell 和 gate 都通过。这是基线已有的本地 shell 版本限制,不作为该 PR 的 request-changes 理由。本次未查询或等待远端 CI,也未把 mock metadata 或本机结果宣称为 GitHub queue 的线上验收。启用前仍需维护者确认真实 candidate 的两父 merge/provenance 与两个 required check 行为,不能据本评审自动改变 ruleset。

我的整体评价

APPROVE,附上述非阻断诊断建议。96 增/12 删、五个文件交付的是边界清楚的 workflow 准备增量;既有 DCO、分类器和 gate owner 都复用,没有 speculative runner 或平行状态源。未来向的收敛检查已覆盖输入 guard 与共享资格 owner:guard 是可做的小修,通用抽象不需要。本评审不等于队列已上线、耗时承诺、merge-readiness 或合并授权;本次不合并。

English verdict: APPROVE

songoow and others added 2 commits September 29, 2026 21:14
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
The sonar caller check matched the literal text 'needs: pytest' followed by
'uses:', so adding the merge_group skip between them failed it. Parse the job
instead: it still must need this run's pytest job and call the local reusable
workflow, and its only condition may be the merge_group skip, so a status
function such as always() cannot run analysis without same-run coverage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
@songoow

songoow commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator Author

Updated to exact head aa5b03da7598dfb777cd1e4e898f09b3acab8640.

What changed:

  1. One signed merge of origin/main (649826221) into the branch; it merged cleanly.

  2. test(ci): pin the sonar job structurally, allowing the merge-queue skip. This fixes the failure this PR introduced. tests/test_sonarcloud_workflow.py::test_sonar_reuses_same_run_coverage_without_a_privileged_trigger matched the literal text needs: pytest\n uses: ./.github/workflows/sonarcloud.yml, so the new if: github.event_name != 'merge_group' between those lines broke it. The test now parses the sonar job and asserts needs == "pytest", uses == "./.github/workflows/sonarcloud.yml", and that its only if is exactly the merge_group skip.

    The invariant is intact. Coverage still has to come from the same run's pytest job through the local reusable workflow, and the rest of the test is unchanged: no workflow_run, no pull_request_target, no run-id or github-token fetch. The new test is also stricter than the old one, which never looked at conditions. I mutation-checked it: replacing the condition with always(), changing needs, or dropping the if each fails the test.

Local validation on the new head:

  • pytest tests/test_python_ci_workflow.py tests/test_dco_workflow.py tests/test_sonarcloud_workflow.py tests/test_contributing_guide.py: 129 passed
  • unittest discover -s scripts/ci: 7 tests OK
  • examples/github-actions-runtime-smoke.py: ok

No other test or smoke pins the text of the sonar job or the node-forward condition. The other smokes that match event_name != 'pull_request' read different workflows.

Red checks you can still expect from main: the latest main push run (649826221) fails tests/architecture/test_turn_contract_generation.py::test_new_independent_twin_cannot_hide_behind_generated_pair and tests/control_plane/test_prompt_upgrade_hook.py::test_live_decision_adds_only_existing_required_read_channel[quota_cli_invocation|loopx_turn_run_once]. Both also fail locally on this head. They are not caused by this PR, and they are being fixed separately. The TypeScript host_process.test.ts "descendants cannot keep working" case is a known intermittent flake on unrelated PRs (for example #5304), so treat any failure of it as a flake.

The push dismissed the earlier approval, so I am requesting re-review on this head.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English verdict: APPROVE

没有阻塞性发现;有一处可后续改善的 fail-fast 诊断。按 LoopX PR review capability policy 12,对完整当前 PR 及上次评审后的 Sonar 测试修复重新验证。评审 head:aa5b03da7598dfb777cd1e4e898f09b3acab8640;固定基线:649826221289cd4cb3dd8880d016e0afbbaca0fc。批准的是工作流准备,不是启用在线合并队列、放宽 ruleset 或执行合并。

动机

原有 Sign-off 和 Python qualification 只覆盖 PR、main 等事件,不能为 GitHub 的临时 merge-group candidate 返回对应必需结果。该 PR 是一个有用且可逆的 prerequisite:提供候选验证入口,仍由维护者决定是否启用队列。它没有声称本地 workflow 测试已证明线上排队节省时间,也没有把修改工作流等同于 ruleset 生效。GitHub 官方事件文档明确区分 merge_group 与 pull_request,要求相关必需工作流订阅前者。

改动思路

最强反对理由是临时合并提交可能被错当成普通 PR,继而获得 docs exemption,或 DCO 用错 base/head 导致检查了空贡献范围。PR 沿用真实的 classifier、merge-gate 和 DCO owner,只适配外部事件坐标;非 PR 路径已有 full qualification,队列复用它,不增加第二个资格规则。Doing nothing 不能返回候选 SHA 上的必需结果;另建 queue verifier 会复制范围和合格定义;扩大 DCO 豁免则越过贡献认证边界。

现有 Mergify 冲突/签署提示工作流不是这个 candidate qualification 的 owner,两者不重复。普通 PR、push、手动运行的用户路径保持不变。持续运行只有已有超时、取消及失败修复后重新候选的路径,不引入手工同步状态。下一步有明确 owner 和依赖:维护者以 merge 方法启用队列,确认真实候选 SHA、提交来源与两个必需结果,再决定是否放宽分支最新要求。这是独立可验证的完整准备切片,不是整个在线队列已交付。

具体改动

关键代码讲解

  1. Python Tests merge_group trigger:新事件经已有 non-PR 分支进入 full plan。即使候选的底层修改只是 docs 或 presentation,队列也不享受 PR exemption;已有最终 merge-gate 仍逐一验证所需结果。
  2. Sign-off contribution coordinates:普通 PR 坐标优先,队列使用 merge_group base_ref/head_sha;先去除 refs/heads/ 前缀,再刷新真实目标分支、计算贡献范围。签署要求未降低,GitHub 双父集成提交豁免仍依赖精确 SHA、父节点及已验证的 GitHub 签名来源。
  3. Sonar same-run coverage test:最近修改从脆弱的相邻文本断言改为解析实际 job;必须 needs=pytest、调用本仓库 reusable workflow,且只排除 merge_group。删除条件、改 always、更换 coverage owner 或远端 reusable 的四个突变都使该真实测试失败。工作流不为临时队列引用运行 Sonar;非阻塞 Node forward probe 仍仅在 push/manual 运行。

完整差异六文件、104 行增加与 13 行删除:两个 workflow、已有质量文档及三个耐久测试。没有修改 runtime、权限模型、仓储格式或 UI;不引入新的 caller schema,也不需要 frontend/Lark companion 或第一屏预览。旧评审后的修复只改变 Sonar 断言,但本次结论重新覆盖全部 PR,不继承此前批准。

对主干的风险

实际 Bash 配合隔离 Git remote 验证了普通 PR 与队列:有签署提交通过,未签署提交拒绝;刷新 upstream 后只检查贡献范围。缺失 head、缺失 base、同时缺失坐标均拒绝;真实存在的目标分支下,未签署提交失败,补签后的新提交再通过。完整 ref 正向路径通过,删除前缀归一化会在相同 fixture 的真实 fetch 边界失败。GitHub 提交来源 API 使用合成响应;本次没有制造线上候选或调整在线 ruleset,其真实性 readback 属于启用阶段。

非阻塞建议:DCO 第 32 行的 test base && test head 在 set -e 下不会因左侧 empty-base 立刻退出;目前后面的 Git fetch 拒绝无效 refspec,因此没有放行漏洞。可将两项检查分开,并分别给出缺失坐标的可操作错误,保留本次三个缺失组合的实际 Bash regression。这里要求的是更清晰的故障定位,不要求此 PR 修复其他控制面代码。

本地原生验证:相关 pytest 129 项通过;CI owner unittest 7 项通过;GitHub Actions runtime smoke、Ruff、diff check 通过。标准 premerge canary 首次因磁盘容量不足在创建比较 checkout 时失败,保留记录并回收验证依赖后,同一 head、同一命令重跑的 4 项直接检查和 12 项选中 smoke 全部通过,未缩小范围或放宽预算。真实 classifier/merge-gate 覆盖 PR/push/manual/merge_group 与 docs/presentation/full;缺失或矛盾资格拒绝,修复后重新通过,重排 needs 保持输出。

语义与 CI 对齐

语义选择是适配 GitHub 外部事件、复用本仓库既有 full qualification 与 Sign-off contract。队列触发存在不等于在线激活或授权范围扩大,普通 PR/push/manual 的完整分类、资格诊断和恢复与固定 base 配对保持一致。base/head 的三项既有失败身份及 assertion 详情一致:generated pair 2 != 1,以及两个 read-channel case 的 turn_start_capability_hook_dispatch 差异;只归一化内存地址、耗时,失败 owner 和测试都未改动,归因为 pre_existing_unrelated,不据此 request changes。另保留 base/head 同样的本机 Bash 3.2 nounset 限制。未查询、轮询或等待远端 CI;评审批准不覆盖独立的必需检查/合并就绪约束。

我的整体评价

APPROVE,交付判断为 justified increment。long_horizon 和 user_experience 均保持:已存在的资格、签署、错误修复和重跑流程没有额外确认或手工状态;新的维护者激活步骤提供此前不存在的在线 ruleset 权限,不能省略。未来向重构检查考虑了 classifier、merge-gate、DCO 及 Sonar 的规则 owner,选择保留共同 owner,没有为新事件建立平行验证器;诊断改善可以有界后续处理。残余范围是真实 GitHub 队列候选来源和 hosted 调度,需要维护者启用阶段 readback,而不是把本地 fixture 当成线上验收。没有执行合并。

@huangruiteng
huangruiteng merged commit ebdb9aa into loopx-project:main Sep 30, 2026
24 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants