diff --git a/docs/agent-specs/2026-06-20-planB-phase2-harness/README.md b/docs/agent-specs/2026-06-20-planB-phase2-harness/README.md
index ebdd947b..b5bf69c6 100644
--- a/docs/agent-specs/2026-06-20-planB-phase2-harness/README.md
+++ b/docs/agent-specs/2026-06-20-planB-phase2-harness/README.md
@@ -10,7 +10,7 @@
|---|---|---|---|
| **U7 rubric 评分(R9)** | `rubric_score.py` + `rubric-protocol.md` | ✅ 建好(selftest 7/7) | `--template --batch <批manifest> -o sheet.json` → 人工填分 → `--score sheet.json` 判门 |
| **U8 feed真玩 e2e(R4+R11)** | `u8_feed_play_e2e.py`(接 backend_gw 全链 + play.cdp.cjs/前端 player_cdp + 资产对位断言) | ✅ 建好(selftest 8/8 纯逻辑断言;编排步 gated on gen-done) | `--run --prompt "…" --cdp <端点>`:一句话→SAA 生成→发布→feed→试玩→断言(published/in-feed/资产对位/九门/终局遥测)。play 路二选一见文件注(产品前端 :4173 / 本地 bundle 九门) |
-| **U9 M0 core 联合门(003-U3)** | `u9_m0_core_gate.*`(组合 U8 + 三契约核) | ⏳ 待建 | 跑 U8 e2e + 核 API/dispatcher/manifest 三契约对齐 |
+| **U9 M0 core 联合门(003-U3)** | `u9_m0_core_gate.py`(shell U8 + 运行时读契约结构核) | ✅ 建好(selftest 5/5;读真契约 dispatcher=4键/manifest=8键) | `--run`:跑 U8 e2e + 三契约核(API 信封 code==0 / dispatcher=source-project.schema / manifest=game-package.schema) |
## 门禁判据(plan 003 对齐)
- **U7**:N=8 评分表 + 总均分 ≥ 1.4 + 资产对位项无 0(一票否决)。
@@ -22,3 +22,10 @@ harness 的**逻辑/断言/抽样/聚合**与具体生成产物无关——只
## 串行约束
U8/U9 的 mini-desktop CDP 真跑与 Plan A 的 e2e **串行排队**(同机端口 4320/9222、flock)——见 `.agents/skills/game-e2e-cdp-harness.md`。
+
+## gen-done 接线时须补(诚实标注·非现编)
+本目录 harness 的**逻辑/断言/抽样/三契约核**已建 + selftest;下列须**待 gen-done 产出形态明确后**接线(现无法验,故标 deferred 不伪报):
+1. **U8 play 路二选一**:Plan A 产「staging 发布游戏」→ 走产品前端 :4173 GamePlayer(player_cdp,同 u5u2_walk);产「本地 gameDefinition」→ 走 play.cdp.cjs 本地 bundle 九门。二者断言口径同。
+2. **U8 遥测查询接口**:`backend_gw` 现无 `get_telemetry`,终局断言暂记 gap——gen-done 接线时补遥测查询(或经 DB/日志取 `game_end`)。
+3. **U8→U9 artifacts 传递**:U9 三契约核需 U8 evidence 带 `api_envelope/source_project/package` 三件 artifact;现 U8 未输出(形态待 gen-done 实物定),U9 已对此记 gap。接线时在 `run_e2e` 补 `ev["artifacts"]`。
+4. **抽样源**:U7 `--batch` 须喂 Plan A ≥80% 批 manifest(形态:list[gameId|对象])。
diff --git a/docs/agent-specs/2026-06-20-planB-phase2-harness/u9_m0_core_gate.py b/docs/agent-specs/2026-06-20-planB-phase2-harness/u9_m0_core_gate.py
new file mode 100644
index 00000000..908569c1
--- /dev/null
+++ b/docs/agent-specs/2026-06-20-planB-phase2-harness/u9_m0_core_gate.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+U9 M0 core 联合验收门(Plan B Phase-2 / origin 003-U3)—— turnkey 门。
+
+M0 core = 「一句话真生成可玩游戏 + 三契约对齐」:
+ 1. 跑 U8 e2e(create→生成→发布→feed→真玩,u8_feed_play_e2e.run)= 一句话端到端可玩;
+ 2. **三契约结构核**(运行时读契约 required,防漂移):
+ - API:CommonResult 信封(code==0 + data 非空)—— runtime/package · feed · aigc 关键响应;
+ - dispatcher:SAA 产出 = source-project.schema.json(schemaVersion/sourceHash/profile/gameDefinition);
+ - manifest:游戏包 = game-package.schema.json(schemaVersion/gameId/versionId/templateId/gameConfig/assets/manifest/meta)。
+门:U8 PASS ∧ 三契约全对齐。
+
+**gated on gen-done + U8**:编排步须 Plan A 产可玩游戏后跑(mini-desktop)。本文件先建 + 三契约结构核 selftest 现验。
+"""
+import argparse
+import json
+import os
+import subprocess
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+REPO = os.path.abspath(os.path.join(HERE, "..", "..", "..")) # 仓根(harness 在 docs/agent-specs/
/)
+MANIFEST_SCHEMA = os.path.join(REPO, "contracts", "game-package.schema.json")
+DISPATCHER_SCHEMA = os.path.join(REPO, "contracts", "agent-loop", "source-project.schema.json")
+U8 = os.path.join(HERE, "u8_feed_play_e2e.py")
+
+
+def _required(schema_path):
+ """运行时读契约 required(防硬编码漂移);缺文件抛错。"""
+ with open(schema_path, encoding="utf-8") as f:
+ return json.load(f).get("required") or []
+
+
+def check_required(obj, required):
+ """结构核:required 键齐则 ([], True),否则 (缺键列表, False)。"""
+ obj = obj or {}
+ missing = [k for k in required if k not in obj]
+ return missing, len(missing) == 0
+
+
+# ============== 三契约结构核(gen-done 前可 selftest)==============
+
+def assert_api(envelope):
+ """API 契约:CommonResult 信封 code==0 + data 非空。"""
+ e = envelope or {}
+ ok = (e.get("code") == 0) and (e.get("data") is not None)
+ return ok, {"code": e.get("code"), "has_data": e.get("data") is not None}
+
+
+def assert_dispatcher(source_project):
+ """dispatcher 契约:SAA 产出对齐 source-project.schema required。"""
+ missing, ok = check_required(source_project, _required(DISPATCHER_SCHEMA))
+ return ok, {"missing": missing}
+
+
+def assert_manifest(package):
+ """manifest 契约:游戏包对齐 game-package.schema required。"""
+ missing, ok = check_required(package, _required(MANIFEST_SCHEMA))
+ return ok, {"missing": missing}
+
+
+def three_contracts(api_envelope, source_project, package):
+ """三契约联合核。返回 (passed, detail)。"""
+ a_ok, a = assert_api(api_envelope)
+ d_ok, d = assert_dispatcher(source_project)
+ m_ok, m = assert_manifest(package)
+ return (a_ok and d_ok and m_ok), {"api": {"ok": a_ok, **a},
+ "dispatcher": {"ok": d_ok, **d},
+ "manifest": {"ok": m_ok, **m}}
+
+
+# ============== 联合门(gated on gen-done + U8)==============
+
+def run_m0_core(prompt, cdp_endpoint, out_dir=None):
+ """跑 U8 e2e + 三契约核。返回 verdict dict。"""
+ # 1. U8 e2e(一句话端到端可玩)—— shell u8,读其 JSON 输出
+ cmd = [sys.executable, U8, "--run", "--prompt", prompt, "--cdp", cdp_endpoint]
+ if out_dir:
+ cmd += ["--out", out_dir]
+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
+ try:
+ # u8 末行打 U8_VERDICT,前面是 JSON;取首个 { 到末个对应 }
+ txt = p.stdout
+ u8_ev = json.loads(txt[txt.index("{"):txt.rindex("}") + 1])
+ except Exception:
+ u8_ev = {"passed": False, "_parse_error": (p.stdout or p.stderr)[-400:]}
+ u8_pass = bool(u8_ev.get("passed"))
+
+ # 2. 三契约核 —— 从 U8 evidence 取 artifacts(api 信封 / source-project / manifest)
+ # u8 在 steps 里留有 manifest/gameDef;此处按其 evidence 形态取,缺则记 gap(不伪报)。
+ api_env = (u8_ev.get("artifacts") or {}).get("api_envelope")
+ src_proj = (u8_ev.get("artifacts") or {}).get("source_project")
+ pkg = (u8_ev.get("artifacts") or {}).get("package")
+ tc_pass, tc = three_contracts(api_env, src_proj, pkg)
+ artifacts_present = all(x is not None for x in (api_env, src_proj, pkg))
+
+ passed = u8_pass and tc_pass and artifacts_present
+ return {
+ "passed": passed,
+ "u8_passed": u8_pass,
+ "three_contracts_passed": tc_pass,
+ "artifacts_present": artifacts_present,
+ "three_contracts": tc,
+ "u8": u8_ev,
+ "gap": None if artifacts_present else "U8 evidence 未带 api_envelope/source_project/package 三件 artifact——gen-done 接线时补 u8 artifacts 输出",
+ }
+
+
+# ============== selftest(三契约结构核)==============
+
+def _selftest():
+ # 用真契约文件读 required,构造 好/缺键 fixture
+ req_disp = _required(DISPATCHER_SCHEMA)
+ req_mani = _required(MANIFEST_SCHEMA)
+ assert req_disp and req_mani, "契约 required 读取为空"
+ good_disp = {k: "x" for k in req_disp}
+ good_mani = {k: "x" for k in req_mani}
+ good_api = {"code": 0, "data": {"x": 1}}
+ # 全好 → 过
+ ok, tc = three_contracts(good_api, good_disp, good_mani); assert ok, tc
+ # API code!=0 → 不过
+ ok, tc = three_contracts({"code": 500, "data": None}, good_disp, good_mani)
+ assert (not ok) and (not tc["api"]["ok"]), tc
+ # dispatcher 缺键 → 不过 + 列缺
+ bad_disp = {k: "x" for k in req_disp[1:]} # 缺第一个
+ ok, tc = three_contracts(good_api, bad_disp, good_mani)
+ assert (not ok) and tc["dispatcher"]["missing"] == [req_disp[0]], tc
+ # manifest 缺键 → 不过
+ bad_mani = {k: "x" for k in req_mani[:-1]} # 缺最后一个
+ ok, tc = three_contracts(good_api, good_disp, bad_mani)
+ assert (not ok) and tc["manifest"]["missing"] == [req_mani[-1]], tc
+ # 空对象 → 全缺
+ ok, tc = three_contracts({}, {}, {})
+ assert (not ok) and len(tc["dispatcher"]["missing"]) == len(req_disp), tc
+ print(f"u9_m0_core_gate selftest: 5/5 PASS(三契约结构核;读真契约 dispatcher={len(req_disp)}键 manifest={len(req_mani)}键)")
+
+
+def _print(v):
+ print("=== U9 M0 core 联合门 ===")
+ print(f" U8 e2e(一句话端到端可玩): {'✓' if v['u8_passed'] else '✗'}")
+ print(f" artifacts 齐(api/dispatcher/manifest 三件): {'✓' if v['artifacts_present'] else '✗ ' + (v.get('gap') or '')}")
+ tc = v["three_contracts"]
+ print(f" 三契约: API {'✓' if tc['api']['ok'] else '✗'} | dispatcher {'✓' if tc['dispatcher']['ok'] else '✗ 缺' + str(tc['dispatcher'].get('missing'))} | manifest {'✓' if tc['manifest']['ok'] else '✗ 缺' + str(tc['manifest'].get('missing'))}")
+ print(f" M0_CORE_VERDICT={'PASS' if v['passed'] else 'FAIL'}")
+
+
+def main():
+ ap = argparse.ArgumentParser(description="U9 M0 core 联合门(gated on gen-done + U8)")
+ ap.add_argument("--selftest", action="store_true", help="三契约结构核自测(读真契约;gen-done 前可跑)")
+ ap.add_argument("--run", action="store_true", help="真跑 M0 core(须 gen-done + mini-desktop CDP)")
+ ap.add_argument("--prompt", default="一个躲避陨石的小游戏")
+ ap.add_argument("--cdp", default="http://localhost:9222")
+ ap.add_argument("--out")
+ args = ap.parse_args()
+ if args.selftest:
+ _selftest(); return 0
+ if args.run:
+ v = run_m0_core(args.prompt, args.cdp, args.out)
+ _print(v)
+ return 0 if v["passed"] else 1
+ ap.print_help(); return 2
+
+
+if __name__ == "__main__":
+ sys.exit(main())