diff --git a/AGENTS.md b/AGENTS.md index c5deaf5f..f4549c3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ ## 运行与验证(工作目录 = 含 project.godot 的切片目录) -- **合回验证 = 远端 CI(2026-11-03 用户裁,取代 2026-10-30 本地 CI)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式,环境自举件=`tools/ci/mac_setup.sh`),工作流(`.gitea/workflows/ci.yml`)按改动域分流(分流算法单点=`tools/ci/domain_route.sh`):游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发。回执由 `tools/ci_watch.sh remote` 轮询 Gitea API 写回 `game/tests/.gates/ci-status.json`(合同不变:commit/绿红/红闸(job 级)+job 日志链接/耗时)+`ci-history.jsonl` 流水账;**回执等待一律后台(2026-11-02 用户裁)**:合回落地先向用户呈报本批总结,随即 run_in_background 运行 `tools/ci_watch.sh remote <合并提交>`,禁前台 sleep/轮询等 CI(细则=process-conventions §1 第 3 条)。CI 红态不回滚合并——红项在案、修复批合回即再推再验追绿;开工先读 CI 状态,红未修不开新批。同一份字节禁止第二次人手跑。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起。**runner 依赖**:Mac 需开机在线(关机=远端任务排队延后);换机/重装按 `tools/ci/mac_setup.sh` 自举 +- **合回验证 = 远端 CI(2026-11-03 用户裁,取代 2026-10-30 本地 CI)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式,环境自举件=`tools/ci/mac_setup.sh`),工作流(`.gitea/workflows/ci.yml`)按改动域分流(分流算法单点=`tools/ci/domain_route.sh`):游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发。远端结果**按需现查**:`tools/ci_check.sh <合并提交>` 读该提交任务的 run `status`/`conclusion`,只呈报、不落盘(2026-11-04 用户裁:合回不等待、不唤醒;判据只认 run 落定后的终局态,细则=process-conventions §1 第 3 条)。**合回落地即向用户呈报本批总结并止回合**,禁前台 sleep/轮询等 CI。CI 红态不回滚合并——红项在案、修复批合回即再推再验追绿;开工先跑现查件读 CI 状态,红未修不开新批。同一份字节禁止第二次人手跑。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,文档禁另列清单;`--code`/`--docs`/`--quick` 手动档只服务开发期按域跑批(开发期窄测纪律见 process-conventions §6),不构成验收;全量回归由用户自发发起。**runner 依赖**:Mac 需开机在线(关机=远端任务排队延后);换机/重装按 `tools/ci/mac_setup.sh` 自举 - 单件直跑(调试期最窄确认用,**不构成验收**):冒烟测试 `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 157911e1..b4eeec4b 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -11,8 +11,8 @@ 1. **worktree 车道(一切面)**:每回合先建独立 worktree(建区粒度=回合,会话单区细则见第 5 条):`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-11-03 用户裁取代本地 CI)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式),工作流按改动域分流(分流算法单点=`tools/ci/domain_route.sh`:游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发)——唯一强制验证点。回执由 `tools/ci_watch.sh remote` 轮询 Gitea API 写回 `game/tests/.gates/ci-status.json`(合同不变:commit/verdict/red_gates/key_lines/note),agent 开工先读 CI 状态的纪律不变,红未修不开新批。CI 红态不回滚合并——修复走新开修复 worktree(`fix-<任务名>` 分支,非 iter 形免重塑免数卡),修复合回落地即再推再验追绿(追绿同链;本地跑批件 `ci_runner.sh` 与本地追绿钩子 post-commit 已退役)。导入缓存刷新(.godot 不入 git 的在册坑)由工作流「导入缓存刷新」步接管。**回执等待纪律(2026-11-02 用户裁)**:合回落地即向用户呈报本批总结,回执等待一律以 run_in_background 后台任务运行 `tools/ci_watch.sh remote <合并提交>`(认领判据与容错细则=该件头注),禁前台 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 钩子——远端 CI 触发(推送+挂回执等待;仅主工作区,任务 worktree 内的合并不触发)。③git 层不设车道钩子(pre-commit 直改闸已退役)——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层) +3. **合回后验证(远端 CI,2026-11-03 用户裁取代本地 CI)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式),工作流按改动域分流(分流算法单点=`tools/ci/domain_route.sh`:游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发)——唯一强制验证点。远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读该提交任务的 run 自身 `status`/`conclusion`,只呈报、不落盘),agent 开工先读 CI 状态的纪律不变,红未修不开新批。CI 红态不回滚合并——修复走新开修复 worktree(`fix-<任务名>` 分支,非 iter 形免重塑免数卡),修复合回落地即再推再验追绿(追绿同链;本地跑批件 `ci_runner.sh` 与本地追绿钩子 post-commit 已退役)。导入缓存刷新(.godot 不入 git 的在册坑)由工作流「导入缓存刷新」步接管。**合回零等待纪律(2026-11-04 用户裁,取代 2026-11-02 回执等待纪律)**:合回落地即向用户呈报本批总结并止回合——不挂后台轮询、不等回执、不被唤醒;CI 结论什么时候要看就现查一次(`tools/ci_check.sh`,run 那时早已落定,秒级)。退役缘由=原轮询件把「commit status 离开 pending」当终局判据,会把 run 未落定期的尝试级失败写成终局红(run 37 实证:开跑 36 秒写红,该 run 三分半后 `conclusion=success`),且零触发批与 runner 排队必熬满超时再写红;同 commit 的多路轮询还重复覆写同一份判定。跑闸等一切分钟级跑批的等待纪律不变——后台运行,禁前台 sleep/轮询循环。 +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 钩子——远端 CI 触发(只推送,不挂等待;仅主工作区,任务 worktree 内的合并不触发)。③git 层不设车道钩子(pre-commit 直改闸已退役)——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层) 5. **会话单区(2026-11-04 用户裁)**:建区粒度=回合——同一时刻每会话至多一个 worktree 在役;合入删区之前,会话内用户追加的事项一律并入在役区推进,禁为追加事项另开新区。区内全部事项完成后只做一次合入(一次 `git merge --no-ff`、post-merge 一次推送、远端 CI 只跑一次),追加事项在合入提交信息里分列;无改动需求的事项不建区。合回删区后用户再发令的事项=新回合,另开新区(区名取该回合主任务名)。CI 红的 `fix-<任务名>` 修复区属同一回合的追绿延续,不另算新区。迭代实现批(iter 形分支)独占回合——其他类事项不得并入迭代区(合回守卫数卡校验本就拦未归卡提交),待其合回后按新回合另开。 ## 2. 五段流水线 @@ -49,7 +49,7 @@ 7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补 **完成** -8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),游戏码批跑全量域闸(不带参数档,02 全套件),判定与红项由 `ci_watch.sh remote` 写回 `game/tests/.gates/ci-status.json`。全量是游戏码批的强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——02 只跑「需求卡片申报 ∪ diff 推导」并集套件、00 冒烟/01 系统测试/09 静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留) +8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),游戏码批跑全量域闸(不带参数档,02 全套件),判定与红项用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 终局态,只呈报不落盘)。全量是游戏码批的强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——02 只跑「需求卡片申报 ∪ diff 推导」并集套件、00 冒烟/01 系统测试/09 静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留) 9. **重跑语义(2026-10-30 用户裁,取代原「每批一次/合并后零复跑」口径)**——同一份字节禁止第二次人手跑;允许的重跑只剩两类:① CI 红后的修复合回(post-merge 钩子再推再验,每轮修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。导入缓存刷新由远端工作流「导入缓存刷新」步接管,合并后无手工动作 10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12 11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注 diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 34e25a54..a8fa1e4e 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -27,7 +27,7 @@ ## 4. 验证时机(合回后远端 CI 制,2026-11-03 换代) -开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、02 全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),回执由 `tools/ci_watch.sh remote` 轮询写回 `game/tests/.gates/ci-status.json`;CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1;本地跑批件 ci_runner.sh 与本地追绿钩子 post-commit 已退役)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 +开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、02 全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 自身 `status`/`conclusion`,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1;本地跑批件 ci_runner.sh 与本地追绿钩子 post-commit 已退役)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。 ## 5. 测试函数怎么写——验收清单条目的落码规范 @@ -128,7 +128,7 @@ 3. 检查测试削弱:删除断言、放宽阈值、缩小扫描面、换下层直调,逐处要卡片申报或用户批准。 4. 核对内部字段直查的受控例外:直查行是否已在对照表注明理由(该行为无公开可观测面可钉)。 -**审计深度分级(2026-11-04 立)**:审计按卡内申报的验收档(§8.3)逐卡定深度;梯度只落在昂贵的判断件上,机械且堵造假向量的两件(削弱检查、例外核对)三档全保留不抽。中/低档省略的测试复跑以 CI 回执为执行证据(回执是机器产物——「不采信实现者自述已全绿」的口径不变): +**审计深度分级(2026-11-04 立)**:审计按卡内申报的验收档(§8.3)逐卡定深度;梯度只落在昂贵的判断件上,机械且堵造假向量的两件(削弱检查、例外核对)三档全保留不抽。中/低档省略的测试复跑以远端 CI 读数为执行证据——`tools/ci_check.sh <合并提交>` 的现查输出(commit+run 号+conclusion)是机器产物,run 号在远端永久可复核,审计报告照抄该行即可(2026-11-04 用户裁:回执等待件退役,证据由落盘文件改为现查读数;「不采信实现者自述已全绿」的口径不变): | 审计动作 | 高 | 中 | 低 | |---|---|---|---| @@ -136,7 +136,7 @@ | 判据绑定面核对 | 抽查(现行口径) | 抽查(现行口径) | 抽 1 行 | | 测试削弱检查 | 逐处 | 逐处 | 逐处 | | 直查例外核对 | 逐条 | 逐条 | 逐条 | -| 最窄测试复跑 | 抽查(现行口径) | 省略——以 CI 回执代证 | 省略——以 CI 回执代证 | +| 最窄测试复跑 | 抽查(现行口径) | 省略——以远端 CI 现查读数代证 | 省略——以远端 CI 现查读数代证 | 低档的抽查面与抽中依据照写审计报告;「每批必有审计、禁零审计批」不变;档位存疑按高档执行。 diff --git a/game/tests/.gates/.gitignore b/game/tests/.gates/.gitignore index 7bba17d4..7f5a2833 100644 --- a/game/tests/.gates/.gitignore +++ b/game/tests/.gates/.gitignore @@ -1,5 +1,5 @@ # 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。 -# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是本地 CI 判定与流水账、 +# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是已退役的 CI 回执判定与流水账(判定只在远端,本机残留)、 # ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。 receipt.json ci-status.json diff --git a/tools/ci_check.sh b/tools/ci_check.sh new file mode 100755 index 00000000..5ec916a7 --- /dev/null +++ b/tools/ci_check.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# CI 现查件——按需读远端 run 的终局态,只呈报、不落盘(2026-11-04 用户裁:回执等待件与本地缓存判定退役)。 +# 用法: tools/ci_check.sh [<提交hash>](缺省=当前 HEAD) +# 判据:Gitea commit status 只用来取本次任务的 run 链接,绿红一律认 run 自身的 status/conclusion—— +# run 未 completed(排队/在跑/尝试级中间态)与零触发批(纯闸件/管线批不建任务)一律呈报「未落定」,不写红。 +# 调用面:开工前读态(红未修不开新批)、合回后想知道结果时手动查——合回流程不等待、不唤醒,呈报即回合止。 +# 授权:GITEA_TOKEN 环境变量优先,否则读 git 凭据库(host=git.kuaimeo.com)。 +# 退出码: 0=绿 1=红 2=未落定/零触发/查不到 3=参数或令牌错误 +set -u +COMMON="$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \ + || { echo "不在 git 仓库内"; exit 3; } +REPO="$(dirname "$COMMON")" + +BASE="${1:-}" +[ -n "$BASE" ] || BASE="$(git -C "$REPO" rev-parse HEAD)" +BASE="$(git -C "$REPO" rev-parse --verify "${BASE}^{commit}")" || { echo "提交不存在: $BASE"; exit 3; } + +_origin="$(git -C "$REPO" remote get-url origin 2>/dev/null || true)" +REPO_SLUG="$(printf '%s' "$_origin" | grep -oE '[^/:]+/[^/]+\.git$' | sed 's/\.git$//')" +[ -n "$REPO_SLUG" ] || { echo "无法从 origin 解析 owner/repo: $_origin"; exit 3; } + +TOKEN="${GITEA_TOKEN:-}" +if [ -z "$TOKEN" ]; then + TOKEN="$(printf 'protocol=https\nhost=git.kuaimeo.com\n\n' | git credential fill 2>/dev/null | sed -n 's/^password=//p' | head -1)" +fi +[ -n "$TOKEN" ] || { echo "取不到 Gitea 令牌(设 GITEA_TOKEN 或先 git credential approve 存储)"; exit 3; } +API="https://git.kuaimeo.com/api/v1/repos/$REPO_SLUG" +WEB="https://git.kuaimeo.com/$REPO_SLUG" + +export PYTHONIOENCODING=utf-8 +python -I - "$API" "$WEB" "$BASE" "$TOKEN" <<'PYEOF' +import json, re, sys, urllib.request, urllib.error + +api, web, base, token = sys.argv[1:5] +H = {"Authorization": "token " + token} + + +def get(url, t=20): + req = urllib.request.Request(url, headers=H) + return json.load(urllib.request.urlopen(req, timeout=t)) + + +def short(sha): + return sha[:8] + + +def undetermined(msg): + print("CI现查: 未落定 | commit %s | %s" % (short(base), msg)) + sys.exit(2) + + +try: + s = get(api + "/commits/%s/status" % base) +except urllib.error.HTTPError as e: + undetermined("查 commit status 失败 HTTP %s" % e.code) +except Exception as e: + undetermined("查 commit status 失败:%s" % e) + +statuses = s.get("statuses") or [] +target = next((x.get("target_url", "") for x in statuses if x.get("target_url")), "") +if not statuses: + undetermined("远端无该提交的任务——零触发批(纯闸件/管线批不建任务),或推送未触发 CI") + +m = re.search(r"/actions/runs/(\d+)", target or "") +if not m: + undetermined("commit status 无 run 链接(state=%s),查 %s/actions" % (s.get("state"), web)) +run_id = m.group(1) + +try: + run = get(api + "/actions/runs/%s" % run_id) +except Exception as e: + undetermined("查 run %s 失败:%s" % (run_id, e)) + +rstatus, rconclusion = run.get("status"), run.get("conclusion") +if rstatus != "completed": + undetermined("run %s status=%s(runner 需开机在线;队列与日志见 %s/actions)" % (run_id, rstatus, web)) + +if rconclusion == "success": + print("CI现查: 绿 | commit %s | run %s conclusion=success" % (short(base), run_id)) + sys.exit(0) + +red_jobs = [] +try: + jobs = get(api + "/actions/runs/%s/jobs" % run_id) + for j in jobs.get("jobs", []): + if j.get("conclusion") in ("success", "skipped", None): + continue + bad_steps = [x.get("name") for x in (j.get("steps") or []) if x.get("conclusion") not in ("success", "skipped")] + red_jobs.append((j.get("name"), bad_steps)) +except Exception as e: + red_jobs.append(("" % e, [])) + +print("CI现查: 红 | commit %s | run %s conclusion=%s" % (short(base), run_id, rconclusion)) +for name, steps in red_jobs: + print(" 红 job:%s" % name) + for st in steps: + print(" 失败步:%s" % st) +print(" 日志:%s" % (target or "%s/actions/runs/%s" % (web, run_id))) +sys.exit(1) +PYEOF diff --git a/tools/ci_watch.sh b/tools/ci_watch.sh deleted file mode 100755 index 13b2b5e9..00000000 --- a/tools/ci_watch.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# 回执等待件——合回后等远端 CI 回执的后台轮询器(2026-11-03 用户裁:CI 迁远端,本件改轮询 Gitea API)。 -# 用法: tools/ci_watch.sh remote <推送提交hash> [超时秒,默认 3000] -# 调用面: post-merge 钩子推送 master 成功后自动后台挂起;人手等待亦可(agent 会话以 run_in_background 运行, -# 命令结束时 harness 唤醒——绿→删区收口;红→呈报红闸名单走修复批;超时→呈报「回执未落」)。 -# 「本次回执已落」判据:Gitea commit status 的 state 离开 pending(success/failure/error), -# 且 status 的 target_url 指向本仓库 Actions(防认错仓)。远端跑批 20-35 分钟量级,默认超时给足 50 分钟。 -# 产物:沿用本地 CI 时代的回执合同——game/tests/.gates/ci-status.json(commit/verdict/timestamp/ -# duration_s/red_gates/key_lines/note)覆写 + ci-history.jsonl 追加;红态 red_gates=失败的 job 名单 -# (来自 runs//jobs API),job 日志链接在 key_lines,细粒度红闸清单以 job 日志为准。 -# 授权:GITEA_TOKEN 环境变量优先,否则读 git 凭据库(host=git.kuaimeo.com)。 -# 退出码: 0=绿 1=红 2=超时/回执未落 3=参数错误 -set -u -COMMON="$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \ - || { echo "不在 git 仓库内"; exit 3; } -REPO="$(dirname "$COMMON")" -GATES_DIR="$REPO/game/tests/.gates" -STATUS="$GATES_DIR/ci-status.json" -HISTORY="$GATES_DIR/ci-history.jsonl" - -MODE="${1:-}" -BASE="${2:-}" -TIMEOUT="${3:-3000}" -INTERVAL=30 -case "$MODE" in - remote) ;; - *) - echo "用法: ci_watch.sh remote <推送提交hash> [超时秒](本地 CI 模式已退役——回执现由远端产生)" - exit 3 ;; -esac -[ -n "$BASE" ] || { echo "用法: ci_watch.sh remote <推送提交hash> [超时秒]"; exit 3; } -BASE="$(git -C "$REPO" rev-parse --verify "${BASE}^{commit}")" || { echo "提交不存在: $BASE"; exit 3; } - -# 仓库全名(owner/repo)从 origin URL 解析(HTTPS 与 SSH 形式都兼容) -_origin="$(git -C "$REPO" remote get-url origin 2>/dev/null || true)" -REPO_SLUG="$(printf '%s' "$_origin" | grep -oE '[^/:]+/[^/]+\.git$' | sed 's/\.git$//')" -[ -n "$REPO_SLUG" ] || { echo "无法从 origin 解析 owner/repo: $_origin"; exit 3; } - -# 令牌:环境变量优先,回落 git 凭据库 -TOKEN="${GITEA_TOKEN:-}" -if [ -z "$TOKEN" ]; then - TOKEN="$(printf 'protocol=https\nhost=git.kuaimeo.com\n\n' | git credential fill 2>/dev/null | sed -n 's/^password=//p' | head -1)" -fi -[ -n "$TOKEN" ] || { echo "取不到 Gitea 令牌(设 GITEA_TOKEN 或先 git credential approve 存储)"; exit 3; } -API="https://git.kuaimeo.com/api/v1/repos/$REPO_SLUG" - -export PYTHONIOENCODING=utf-8 -python -I - "$API" "$BASE" "$TOKEN" "$TIMEOUT" "$INTERVAL" "$STATUS" "$HISTORY" <<'PYEOF' -import json, os, re, subprocess, sys, time, urllib.request, urllib.error, datetime - -api, base, token, timeout, interval, status_path, history_path = sys.argv[1:8] -timeout, interval = int(timeout), int(interval) -deadline = time.time() + timeout -start = time.time() -H = {"Authorization": "token " + token} - - -def get(url, t=20): - req = urllib.request.Request(url, headers=H) - return json.load(urllib.request.urlopen(req, timeout=t)) - - -def combined_status(sha): - return get(api + "/commits/%s/status" % sha) - - -def write_receipt(verdict, note, red_gates, key_lines): - now = datetime.datetime.now().astimezone().isoformat(timespec="seconds") - dur = int(time.time() - start) - d = {"commit": base, "verdict": verdict, "timestamp": now, - "duration_s": dur, "note": note, "red_gates": red_gates, "key_lines": key_lines} - os.makedirs(os.path.dirname(status_path), exist_ok=True) - tmp = status_path + ".tmp" - with open(tmp, "w", encoding="utf-8") as f: - json.dump(d, f, ensure_ascii=False, indent=1) - os.replace(tmp, status_path) - with open(history_path, "a", encoding="utf-8") as f: - f.write(json.dumps(d, ensure_ascii=False) + "\n") - return d - - -def emit(d): - 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) - - -last_err = "" -while True: - try: - s = combined_status(base) - state = s.get("state", "") - statuses = s.get("statuses", []) - target = next((x.get("target_url", "") for x in statuses if x.get("target_url")), "") - if state in ("success", "failure", "error", "warning"): - red = state != "success" - red_gates, run_id = [], None - m = re.search(r"/actions/runs/(\d+)", target) - if m and red: - run_id = m.group(1) - try: - jobs = get(api + "/actions/runs/%s/jobs" % run_id) - red_gates = sorted({j["name"] for j in jobs.get("jobs", []) if j.get("status") not in ("success", "skipped")}) - except Exception as e: - red_gates = ["" % e] - note = "远端CI state=%s%s" % (state, ("(run %s)" % run_id) if run_id else "") - key_lines = ["job日志: %s" % target] if target else [] - d = write_receipt("green" if not red else "red", note, red_gates, key_lines) - emit(d) - sys.exit(0 if not red else 1) - # pending:继续等 - except urllib.error.HTTPError as e: - last_err = "HTTP %s" % e.code - except Exception as e: - last_err = str(e) - if time.time() > deadline: - note = "超时(%ds)远端回执未落——最后错误 %s;查 runner 在线态与 %s/actions" % (timeout, last_err or "无", api.rsplit("/repos/", 1)[0]) - d = write_receipt("red", note, ["<回执未落>"], []) - emit(d) - sys.exit(2) - time.sleep(interval) -PYEOF diff --git a/tools/hooks/post-merge b/tools/hooks/post-merge index f0ca94c6..e3d26782 100644 --- a/tools/hooks/post-merge +++ b/tools/hooks/post-merge @@ -1,27 +1,19 @@ #!/usr/bin/env bash -# 合回验证点(远端 CI 触发钩子,2026-11-03 用户裁)——主工作区 git merge 落地后: -# ① push origin master(触发远端 CI:Gitea 收到 push → 派发 mac-ci runner 按改动域跑闸) -# ② 后台挂 ci_watch.sh remote 等回执(回执落 ci-status.json,agent 下次开工读态零改动) -# 推送失败=远端验证不可能发生——大声呈报并落 push_failed 判定进 ci-status.json(防静默,下游开工读态即见)。 +# 合回验证触发件(远端 CI 推送钩子,2026-11-03 用户裁;2026-11-04 用户裁去掉回执等待)。 +# 主工作区 git merge 落地后做一件事:`git push origin master` 触发远端 CI(Gitea Actions, +# runner=Mac 本机 host 模式,按改动域分流跑闸)。判定结果不在本钩子取——合回即呈报本批总结, +# 回合止;要看结果随时跑 tools/ci_check.sh <合并提交>(只呈报、不落盘,2026-11-04 取代原后台轮询件)。 +# 推送失败=远端验证不可能发生——大声呈报并给出处置(先修推送再手动 git push,之后用现查件读态),禁静默。 # 任务 worktree 内的合并不触发(CI 只认 master);CI 红了不回滚合并——修复批合回后本钩子再推再验(追绿同链)。 set -u _GD="$(cd "$(git rev-parse --git-dir)" && pwd)" _CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)" [ "$_GD" = "$_CD" ] || exit 0 -REPO="$(git rev-parse --show-toplevel)" +REPO="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || pwd)" TIP="$(git -C "$REPO" rev-parse HEAD)" -export PYTHONIOENCODING=utf-8 if ! git -C "$REPO" push origin master >/dev/null 2>&1; then - echo "[post-merge] ✗ push origin master 失败——远端 CI 无法触发!先修推送(凭据/网络)后手动 git push,再手动挂 tools/ci_watch.sh remote $TIP" - python -I -c " -import json, os, datetime -p = r'$REPO/game/tests/.gates/ci-status.json' -d = {'commit': r'$TIP', 'verdict': 'push_failed', 'timestamp': datetime.datetime.now().astimezone().isoformat(timespec='seconds'), - 'duration_s': 0, 'note': 'post-merge 推送失败,远端 CI 未触发——修复推送后手动 git push + ci_watch remote', 'red_gates': [], 'key_lines': []} -os.makedirs(os.path.dirname(p), exist_ok=True) -json.dump(d, open(p, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) -" 2>/dev/null + echo "[post-merge] ✗ push origin master 失败——远端 CI 无法触发!先修推送(凭据/网络),再手动 git push,然后用 tools/ci_check.sh $TIP 读态。" exit 0 fi -( nohup bash "$REPO/tools/ci_watch.sh" remote "$TIP" >/dev/null 2>&1 & ) +echo "[post-merge] ✓ 已推送 origin master(commit ${TIP:0:8})——远端 CI 已触发;结果不等待,随时用 tools/ci_check.sh ${TIP:0:8} 现查。" exit 0 diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 2ad1dc41..3607368f 100755 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -4,9 +4,9 @@ # 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。 # # 跑完写回执 game/tests/.gates/receipt.json(commit+内容树哈希+各闸退出码+存量基线比对+时间戳); -# 远端 CI(2026-11-03 用户裁)读回执判定,回执经 tools/ci_watch.sh remote 轮询 Gitea API 写 ci-status.json—— -# 强制验证点=合回 master 后 post-merge 钩子推送触发的远端跑批;本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。 -# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节),CI 判定照常落账留痕。 +# 强制验证点=合回 master 后 post-merge 钩子推送触发的远端跑批;远端结果用 tools/ci_check.sh 现查(只呈报、 +# 不落盘——2026-11-04 用户裁去掉回执等待件与本地判定缓存)。本脚本手动调用(--code/--docs/全量档)只服务开发期按域跑批。 +# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节)。 # # 用法:tools/run_all_gates.sh [--code|--docs|--quick] # --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/ @@ -86,7 +86,7 @@ fi # 每次跑批前自愈重建,避免「清目录重跑」把 .gitignore 一起带走。 mkdir -p "$STAMP_DIR" if [ ! -f "$STAMP_DIR/.gitignore" ]; then - printf '# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。\n# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是 CI 回执判定与流水账(判定由远端 CI 产生)、\n# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。\nreceipt.json\nci-status.json\nci-history.jsonl\nci.lock\nci.rerun\n*.log\n' \ + printf '# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。\n# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是已退役的 CI 回执判定与流水账(判定只在远端,本机残留)、\n# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。\nreceipt.json\nci-status.json\nci-history.jsonl\nci.lock\nci.rerun\n*.log\n' \ > "$STAMP_DIR/.gitignore" echo "[gates] 已自愈重建 $STAMP_DIR/.gitignore" fi diff --git a/待办清单.md b/待办清单.md index b55eb019..6562af40 100644 --- a/待办清单.md +++ b/待办清单.md @@ -255,14 +255,6 @@ **状态**:待文档设计 -#### ci_watch 中间态误写终局红回执(2026-11-04 登记,来源=批次移交) - -**实况**:2026-11-04 run 37(commit 536dc072)实证——ci-status.json 于 16:59:55 写入 verdict=red(note=远端CI state=failure, run 37),而该 run 实际 17:03:10 才以「Job succeeded」落定,现查 Gitea API 组合状态=success、run conclusion=success;回执与真值相反,下游按「红未修不开新批」被误阻塞(验收档与条目限长批开工时人工查 API 解封)。证据=job 日志结尾「Job succeeded」(Gitea API actions/jobs/38/logs)+commits/536dc072/status 现查 success。修复方向=轮询只认 run conclusion 落定后的终局态(pending/中间态不写盘继续等)。 - -**改动面**:工具代码 - -**状态**:待执行 - #### minesweeper_rules 公开面还债——S13 超标键撤销后的清偿(2026-11-04 登记,来源=批次移交,优先级=P0) **实况**:证据=tools/gdstyle_baseline.txt(本批撤销无凭证键后,gdstyle 闸红单键:game/core/minesweeper_rules.gd 公开方法 21>上限 20);+2 搬家方法 harmful_variants/harmful_adjacent 为迭代51卡② R2「五方法归位」裁决的直接后果(git 提交 7f512aa8),基点漂移考证:写卡基点 6261b36e 实数 18,iter50-卡3 a2886a53 +1 → 实现基点 19,+2=21。还债方向:settle_batch/fx_reveal 族收敛或搬迁,公开方法 21→≤20;还债批合回后 gdstyle 归绿(CI 追绿),期间代码域 CI 红在案、文档批不受影响(域分流)。