From e340f14e87193502678a0cec9a4117377629eabb Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 2 Nov 2026 00:10:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B+=E8=A7=84=E5=88=99(CI?= =?UTF-8?q?=E5=9B=9E=E6=89=A7=E7=AD=89=E5=BE=85=E5=BC=82=E6=AD=A5=E5=8C=96?= =?UTF-8?q?=C2=B7=E7=94=A8=E6=88=B7=E4=BB=A4):=20ci=5Fwatch.sh=E6=96=B0?= =?UTF-8?q?=E4=BB=B6(=E8=AE=A4=E9=A2=86=E5=88=A4=E6=8D=AE=3D=E5=9B=9E?= =?UTF-8?q?=E6=89=A7commit=E4=B8=BA=E5=90=88=E5=B9=B6=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=88=96=E5=90=8E=E4=BB=A3+=E5=9B=9E=E6=89=A7=E6=97=B6?= =?UTF-8?q?=E5=88=BB=E6=99=9A=E4=BA=8E=E5=90=88=E5=B9=B6,=E5=8D=8A?= =?UTF-8?q?=E6=88=AAJSON=E9=87=8D=E8=AF=95,900s=E8=B6=85=E6=97=B6=E9=98=B2?= =?UTF-8?q?=E6=8C=82=E6=AD=BB,=E7=BB=8Fgit=E5=85=AC=E5=85=B1=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=81=92=E8=AF=BB=E4=B8=BB=E5=8C=BA=E5=9B=9E=E6=89=A7?= =?UTF-8?q?)+process-conventions=C2=A71.3=E5=9B=9E=E6=89=A7=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E7=BA=AA=E5=BE=8B+AGENTS.md=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E4=B8=8E=E9=AA=8C=E8=AF=81=E4=B8=80=E5=8F=A5+iter=E6=8A=80?= =?UTF-8?q?=E8=83=BD=E6=80=BB=E6=94=B6=E5=8F=A3=E6=AD=A5=E6=8D=A2=E4=BB=A3?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=90=88=E5=9B=9E=E8=90=BD=E5=9C=B0=E5=85=88?= =?UTF-8?q?=E5=91=88=E6=8A=A5=E6=80=BB=E7=BB=93,=E5=9B=9E=E6=89=A7?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E4=B8=80=E5=BE=8Brun=5Fin=5Fbackground?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E4=BB=BB=E5=8A=A1,=E7=A6=81=E5=89=8D?= =?UTF-8?q?=E5=8F=B0sleep/=E8=BD=AE=E8=AF=A2;=E4=B8=A4=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=AE=9E=E8=AF=81(=E5=90=88=E5=B9=B6=E2=86=92=E5=91=88?= =?UTF-8?q?=E6=8A=A5=E9=98=BB=E5=A1=9E20.5=E5=88=86=E9=92=9F/=E5=85=A8?= =?UTF-8?q?=E5=A4=A9=E5=89=8D=E5=8F=B0sleep=E7=AD=89=E9=97=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- design/engineering/process-conventions.md | 2 +- tools/ci_watch.sh | 57 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100755 tools/ci_watch.sh diff --git a/AGENTS.md b/AGENTS.md index c30947c5..f88ccbb6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ ## 运行与验证(工作目录 = 含 project.godot 的切片目录) -- **合回验证 = 本地 CI(2026-10-30 用户裁)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh` 跑一次全量域闸(调 `tools/run_all_gates.sh` 不带参数档,02 全套件),结果落 `game/tests/.gates/ci-status.json`(commit/绿红/红闸名单+关键日志行/耗时)+`ci-history.jsonl` 流水账;CI 红态下修复合回落地即由 post-merge 钩子复验追绿(红态修复走新开修复 worktree,`fix-<任务名>` 分支,免重塑免数卡;post-commit 钩子保留为红态主区直提兜底触发,绿态与无状态零触发)——批内提交零钩子检查、合回只验一次,同一份字节禁止第二次人手跑。CI 红不回滚合并,红项在案、修复合回追绿;开工先读 CI 状态,红未修不开新批。跑批起手自动 `--editor --quit` 刷类名缓存(.godot 缓存不入 git,防合并后失配假红)。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起 +- **合回验证 = 本地 CI(2026-10-30 用户裁)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh` 跑一次全量域闸(调 `tools/run_all_gates.sh` 不带参数档,02 全套件),结果落 `game/tests/.gates/ci-status.json`(commit/绿红/红闸名单+关键日志行/耗时)+`ci-history.jsonl` 流水账;CI 红态下修复合回落地即由 post-merge 钩子复验追绿(红态修复走新开修复 worktree,`fix-<任务名>` 分支,免重塑免数卡;post-commit 钩子保留为红态主区直提兜底触发,绿态与无状态零触发)——批内提交零钩子检查、合回只验一次,同一份字节禁止第二次人手跑。CI 红不回滚合并,红项在案、修复合回追绿;开工先读 CI 状态,红未修不开新批。跑批起手自动 `--editor --quit` 刷类名缓存(.godot 缓存不入 git,防合并后失配假红)。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,闸件增减与分域调整都只改它,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起。**回执等待一律后台(2026-11-02 用户裁)**:合回落地先向用户呈报本批总结,随即 run_in_background 运行 `tools/ci_watch.sh <合并提交>` 等回执、唤醒后再续,禁前台 sleep/轮询等 CI(细则=process-conventions §1 第 3 条) - 单件直跑(调试期最窄确认用,**不构成验收**):冒烟测试 `python tools/run_smoke.py`(`game/tests/smoke_boot.tscn` 极小启动场景)· 系统测试 `python tools/run_system_test.py`(宿主 `game/tests/system_test.gd`)· 场景探针 `…console.exe --headless --path . res://probe/ui_probe.tscn` · 单元与集成 `… --script res://tests/run_all.gd` - 调参批量:`--script res://sim/adventure_monte_carlo.gd`(全冒险期望)· `res://sim/reward_expectation.gd`(奖励期望);关卡完成率/难度类测量一律走全冒险 - 数值验证:`python tools/balance_verifier.py`(采集端 `res://sim/verifier_batch.gd`,报告与原始数据落 `sim-data/`) diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 09393e77..1efcf908 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -11,7 +11,7 @@ 1. **worktree 车道(一切面)**:每批先建独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`;建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。所有改动与提交都在 worktree 内自由进行——commit 点零钩子检查(`tools/hooks/pre-commit` 已删除)。豁免面仅 `.zcode/`(钩子与配置)与 `.qoder/`(本机 harness 配置)允许主工作区直改——两份实测均不入 git(走 worktree 反而带不进去,改动只对本机生效) 2. **收口顺序(固定)**:**冻结 → 件核 → 并 master → 重塑 → 主区合回 → 删区**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先在 worktree 内 `git merge master` 解冲突(必须在合回之前,合回后才并=CI 验的是中间态);**重塑**=迭代分支(iter 形)合回前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代批免重塑;然后主工作区 `git merge --no-ff <任务分支>`——这是主区唯一合法写动作(合回守卫对迭代分支数卡校验,未归卡提交拦下)。**合回落地即 `git worktree remove`+删分支(合回即删,2026-09-19 原裁);post-merge 钩子已在合回落地时自动触发 CI,红态不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,非 iter 形免重塑免数卡)修复提交再合,落地复验追绿后删修复区**。游戏类测试(冒烟/单测/MC/探针)批内默认零执行(两入口铁规,见 §4),用户点名或当场问才跑 -3. **合回后验证(本地 CI,2026-10-30 用户裁)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh merge` 对 master 尖跑一次全量域闸——唯一强制全量验证点;CI 红态下修复合回落地即由 post-merge 钩子复验追绿(红态修复走新开修复 worktree,`fix-<任务名>` 分支,非 iter 形免重塑免数卡;post-commit 钩子保留为红态主区直提兜底触发,绿态与无状态零触发)。重跑判据:同一份字节禁止第二次人手跑;有意义的重跑只剩「验证修复」一类。CI 红不回滚合并——红项记 `game/tests/.gates/ci-status.json`,开工先读 CI 状态,红未修不开新批。跑批起手 `--editor --quit` 刷类名缓存(在册坑:.godot 缓存不入 git,合并后失配假红假绿)。并跑语义=跑完补跑(coalesce):跑批期间新触发不中断当前轮,轮尾对最新尖端补跑,绝不逐笔各跑一轮 +3. **合回后验证(本地 CI,2026-10-30 用户裁)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)后台触发 `tools/ci_runner.sh merge` 对 master 尖跑一次全量域闸——唯一强制全量验证点;CI 红态下修复合回落地即由 post-merge 钩子复验追绿(红态修复走新开修复 worktree,`fix-<任务名>` 分支,非 iter 形免重塑免数卡;post-commit 钩子保留为红态主区直提兜底触发,绿态与无状态零触发)。重跑判据:同一份字节禁止第二次人手跑;有意义的重跑只剩「验证修复」一类。CI 红不回滚合并——红项记 `game/tests/.gates/ci-status.json`,开工先读 CI 状态,红未修不开新批。跑批起手 `--editor --quit` 刷类名缓存(在册坑:.godot 缓存不入 git,合并后失配假红假绿)。并跑语义=跑完补跑(coalesce):跑批期间新触发不中断当前轮,轮尾对最新尖端补跑,绝不逐笔各跑一轮。**回执等待纪律(2026-11-02 用户裁)**:合回落地即向用户呈报本批总结,回执等待一律以 run_in_background 后台任务运行 `tools/ci_watch.sh <合并提交>`(认领判据与容错细则=该件头注),禁前台 sleep/轮询循环等 CI;后台任务唤醒会话后按回执续行——绿即删区收口,红或超时即呈报走修复批。闸重跑等一切分钟级跑批等待同此纪律 4. **执法分层(全机械,零自觉依赖)**:①`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区 Edit/Write 与主区非 merge `git commit` 拦截(豁免 `.zcode/`、`.qoder/`;worktree 内目标快速放行;合并收尾提交凭 MERGE_HEAD 豁免)、`git commit --no-verify` 拦截(该旗标仅属用户)、iter 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。②post-merge/post-commit 钩子——本地 CI 触发(仅主工作区;任务 worktree 内的合并不触发)。③git 层不设车道钩子(pre-commit 直改闸已退役)——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层) ## 2. 五段流水线 diff --git a/tools/ci_watch.sh b/tools/ci_watch.sh new file mode 100755 index 00000000..79fe2701 --- /dev/null +++ b/tools/ci_watch.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# 回执等待件——合回后等 CI 回执的后台轮询器(2026-11-02 用户裁;规则面=process-conventions §1 第 3 条「回执等待纪律」)。 +# 用法: tools/ci_watch.sh <合并提交hash> [超时秒,默认 900] +# 调用面: 合回落地先向用户呈报本批总结,随即以 run_in_background 后台任务运行本件;命令结束时 +# harness 唤醒会话——绿→删区收口;红→呈报红闸名单走修复批;超时→呈报「回执未落」(CI 可能崩溃)。 +# 「本次回执已落」认领判据(三条全满足,防认错轮): +# ① 回执可整读——CI 写回执非原子,半截/读空静默重试; +# ② 回执 commit 为传入提交或其后代——另一会话合回推进尖端触发补跑覆盖时按后代认领(coalesce 语义见 ci_runner.sh); +# ③ 回执 timestamp 晚于合并提交时刻——排除合并前盘上的旧回执(旧尖端是合并提交的祖先,只查②会误领旧回执)。 +# 退出码: 0=绿 1=红 2=超时/回执未落 3=参数错误 +set -u +# 主工作区定位:CI 只在主工作区跑(post-merge 钩子限主区),从任务 worktree 内调用也要读主区回执—— +# 经 git 公共目录(.git)的上级定位主仓根,主区调用与 worktree 调用同一读数 +COMMON="$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \ + || { echo "不在 git 仓库内"; exit 3; } +REPO="$(dirname "$COMMON")" +STATUS="$REPO/game/tests/.gates/ci-status.json" +BASE="${1:-}" +TIMEOUT="${2:-900}" +INTERVAL=30 +[ -n "$BASE" ] || { echo "用法: ci_watch.sh <合并提交hash> [超时秒]"; exit 3; } +BASE="$(git -C "$REPO" rev-parse --verify "${BASE}^{commit}")" || { echo "提交不存在: $BASE"; exit 3; } +BASE_CT="$(git -C "$REPO" log -1 --format=%ct "$BASE")" +_deadline=$(( $(date +%s) + TIMEOUT )) +while :; do + _read="$(python -I -c " +import json, sys, datetime +try: + d = json.load(open(sys.argv[1], encoding='utf-8')) + ts = int(datetime.datetime.fromisoformat(d['timestamp']).timestamp()) + print(d.get('commit', ''), d.get('verdict', ''), ts) +except Exception: + pass +" "$STATUS")" + _com=""; _verdict=""; _ts="" + if [ -n "$_read" ]; then + set -- $_read + [ $# -eq 3 ] && { _com="$1"; _verdict="$2"; _ts="$3"; } + fi + if [ -n "$_com" ] && [ -n "$_verdict" ] && [ -n "$_ts" ] \ + && [ "$_ts" -gt "$BASE_CT" ] \ + && git -C "$REPO" merge-base --is-ancestor "$BASE" "$_com" 2>/dev/null; then + python -I -c " +import json, sys +d = json.load(open(sys.argv[1], encoding='utf-8')) +print('CI回执:', d.get('verdict'), '| commit', d.get('commit', '')[:8], '| 耗时', d.get('duration_s'), 's') +if d.get('note'): print('note:', d['note']) +for g in d.get('red_gates', []): print('红闸:', g) +for l in d.get('key_lines', []): print('key:', l) +" "$STATUS" + [ "$_verdict" = "green" ] && exit 0 + exit 1 + fi + _now=$(date +%s); _left=$((_deadline - _now)) + [ "$_left" -le 0 ] && { echo "超时(${TIMEOUT}s)回执未落——CI 可能崩溃或未触发,查 game/tests/.gates/ 的 ci.lock 与 ci.log"; exit 2; } + sleep $(( _left < INTERVAL ? _left : INTERVAL )) +done