From 544f1877158b123b00cb4d4c9711e5d1558bc21c Mon Sep 17 00:00:00 2001
From: wlvh <30534800+wlvh@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:40:12 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20PASS=20completion=20?=
=?UTF-8?q?=E9=94=99=E8=AF=AF=E6=88=90=E5=8A=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
en/scripts/OPERATIONS.md | 6 +-
tests/test_sync_coding_workflow.py | 9 +-
tests/test_workflow_sync_skill.py | 534 +++++++++++++++---
zh/scripts/OPERATIONS.md | 32 +-
zh/skills/workflow-docs-sync/evals/README.md | 6 +
.../workflow-docs-sync/scripts/harness.py | 424 ++++++++++++--
6 files changed, 867 insertions(+), 144 deletions(-)
diff --git a/en/scripts/OPERATIONS.md b/en/scripts/OPERATIONS.md
index bb27a39..e882b62 100644
--- a/en/scripts/OPERATIONS.md
+++ b/en/scripts/OPERATIONS.md
@@ -53,6 +53,7 @@ Common rules for every pass:
- Use `.coding_workflow/diffs/pr_body_skeleton.md` or current `PR_BODY.md` as the PR body structure authority.
- Do not modify sync sentinels or content outside sentinel sections.
+- A literal `|` inside a table cell must be written as `\|` or replaced with `
`; only an unescaped `|` separates cells.
- Do not fill `pr_test_evidence`; only the PR submission agent owns it.
- `agent_execution_evidence` is self-reported read coverage for reviewer spot checks only.
- `Full Document Reconcile` must include upstream semantic delta, adopted where, not adopted because, evidence, and downstream impact.
@@ -74,8 +75,9 @@ sections. Current task: execute only PASS 1 - Code Facts / Architecture.
Must read:
1. `.coding_workflow/diffs/agent_workorder.md`
2. `.coding_workflow/diffs/upstream_full/architecture.md`
-3. `PR_BODY.md`; initialize it from `.coding_workflow/diffs/pr_body_skeleton.md`
- if missing; stop if the skeleton is missing.
+3. `PR_BODY.md` must be supplied as the formal handoff artifact before this
+ PASS starts; if it is missing, stop and report a workflow-preparation
+ artifact failure. Do not create it from the skeleton during the PASS.
4. `PR_BODY.md` repo_facts_map and the `architecture.md` row in
full_document_reconcile.
5. Current repo entrypoints, module boundaries, data flow, state model,
diff --git a/tests/test_sync_coding_workflow.py b/tests/test_sync_coding_workflow.py
index 2f8ccda..19c1b3d 100644
--- a/tests/test_sync_coding_workflow.py
+++ b/tests/test_sync_coding_workflow.py
@@ -472,7 +472,9 @@ def test_each_pass_prompt_contains_common_execution_rules(self) -> None:
".coding_workflow/diffs/agent_workorder.md",
".coding_workflow/diffs/pr_body_skeleton.md",
"PR_BODY.md",
- "初始化;如果 skeleton 缺失",
+ "应由 PREPARE 机械创建",
+ "不得自行复制 skeleton",
+ "表格 cell 中的字面 `|` 必须写为 `\\|`",
SYNC_MODULE.SYNC_AUTO_START,
SYNC_MODULE.SYNC_AUTO_END,
"任何 sync sentinel、sentinel 外内容",
@@ -517,10 +519,11 @@ def test_each_pass_prompt_contains_common_execution_rules(self) -> None:
)[0]
self.assertNotIn("本文档", pass_text)
self.assertEqual(
- pass_text.count("如果不存在,先用"),
+ pass_text.count("应由 PREPARE 机械创建"),
1,
- msg=f"{title} prompt should define PR_BODY init once",
+ msg=f"{title} prompt should bind PR_BODY to PREPARE once",
)
+ self.assertNotIn("如果不存在,先用", pass_text)
self.assertNotIn("回报修改了哪些文件", pass_text)
self.assertNotIn("回报是否写入 downstream impact", pass_text)
self.assertNotIn("PR body sections", pass_text)
diff --git a/tests/test_workflow_sync_skill.py b/tests/test_workflow_sync_skill.py
index 2e42f41..ca72293 100644
--- a/tests/test_workflow_sync_skill.py
+++ b/tests/test_workflow_sync_skill.py
@@ -60,21 +60,21 @@
)
-def load_sync_module() -> Any:
- """加载 sync 实现,返回机械 PASS ownership 真相源。"""
- module_path = REPO_ROOT / "scripts/sync_coding_workflow.py"
- spec = importlib.util.spec_from_file_location(
- "sync_coding_workflow_skill_test",
- module_path,
- )
+def load_module(*, module_path: Path, module_name: str) -> Any:
+ """加载指定 Python 文件,返回测试所需的真实生产模块。"""
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is None or spec.loader is None:
- raise AssertionError("无法加载 sync_coding_workflow.py")
+ raise AssertionError(f"无法加载 {module_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
-SYNC_MODULE = load_sync_module()
+SYNC_MODULE = load_module(
+ module_path=REPO_ROOT / "scripts/sync_coding_workflow.py",
+ module_name="sync_test",
+)
+HARNESS_MODULE = load_module(module_path=HARNESS, module_name="harness_test")
def run_command(
@@ -196,6 +196,9 @@ def create_upstream(
"printf 'workorder\\n' > "
".coding_workflow/diffs/agent_workorder.md\n"
"printf '{}\\n' > .coding_workflow/diffs/sync_state.json\n"
+ "[ ! -f PR_BODY.md ] || "
+ "[ -f .coding_workflow/diffs/pr_body_skeleton.md ] || "
+ "cp PR_BODY.md .coding_workflow/diffs/pr_body_skeleton.md\n"
"if [ \"$#\" -eq 1 ] && [ \"$1\" = \"--final\" ]; then\n"
f"{final_lines}"
"fi\n"
@@ -207,19 +210,46 @@ def create_upstream(
def pr_body_text() -> str:
- """返回包含全部 stable sentinel 的最小测试 PR body。"""
- sections: list[str] = [""]
- for section_name in AGENT_SECTIONS:
- sections.extend(
- (
- f"",
- f"## {section_name}",
- "",
- f"{section_name} baseline",
- f"",
- )
+ """返回保留真实 heading、表头和全部逐 PASS 行的 sentinel body。"""
+ reconcile = [
+ "## Full Document Reconcile",
+ "",
+ "| " + " | ".join(SYNC_MODULE.FULL_RECONCILE_COLUMNS) + " |",
+ "| "
+ + " | ".join("---" for _ in SYNC_MODULE.FULL_RECONCILE_COLUMNS)
+ + " |",
+ ]
+ for sync_pass in SYNC_MODULE.SYNC_PASSES:
+ for relative_path in sync_pass["files"]:
+ reconcile.append(
+ f"| {sync_pass['title']} | `{relative_path}` | `fixture` | "
+ "待补充 | 待补充 | 待补充 | 待补充 | 待补充 |"
+ )
+ contents = (
+ SYNC_MODULE.render_repo_facts_template(),
+ "\n".join(reconcile),
+ SYNC_MODULE.render_pr_test_evidence_template(),
+ SYNC_MODULE.render_upstream_drift_log_template(),
+ SYNC_MODULE.render_agent_execution_evidence_template(),
+ SYNC_MODULE.render_remaining_human_decisions_template().replace(
+ "- none", "- 待判断: fixture\n- concrete fixture decision"),
+ )
+ body = [
+ SYNC_MODULE.SYNC_PR_BODY_MARKER,
+ SYNC_MODULE.wrap_agent_section(
+ section_name="repo_facts_map",
+ content=contents[0],
+ ),
+ "\nfixture\n",
+ ]
+ body.extend(
+ SYNC_MODULE.wrap_agent_section(
+ section_name=section_name,
+ content=content,
)
- return "\n".join(sections) + "\n"
+ for section_name, content in zip(AGENT_SECTIONS[1:], contents[1:])
+ )
+ return "\n\n".join(body) + "\n"
def initialize_core_target(
@@ -361,6 +391,17 @@ def run_harness(
)
+def finish_one(*, target: Path, upstream: Path, sha: str) -> Any:
+ """运行 PASS_1 finish,返回可供连续失败重试断言的结果。"""
+ return run_harness(
+ command="finish-pass",
+ target=target,
+ upstream=upstream,
+ upstream_sha=sha,
+ mode="PASS_1",
+ )
+
+
def append_text(*, path: Path, text: str) -> None:
"""向 UTF-8 文件末尾追加测试内容。"""
path.write_text(
@@ -391,6 +432,77 @@ def replace_section(
)
+def section_text(*, path: Path, section_name: str) -> str:
+ """读取指定 sentinel section,返回不含边界标记的原始文本。"""
+ content = path.read_text(encoding="utf-8")
+ start = f""
+ end = f""
+ return content[content.index(start) + len(start):content.index(end)]
+
+
+def complete_pass_body(*, path: Path, mode: str) -> None:
+ """只完成当前 PASS 的既有 heading/owned rows/evidence row。"""
+ ownership = json.loads(OWNERSHIP.read_text(encoding="utf-8"))
+ entry = ownership[mode]
+ if mode == "PASS_1":
+ repo_facts = section_text(
+ path=path,
+ section_name="repo_facts_map",
+ )
+ replace_section(
+ path=path,
+ section_name="repo_facts_map",
+ text=repo_facts.replace("证据: 待补充", "证据: fixture evidence"),
+ )
+
+ # 逐行替换能保留表头、未来 PASS 行和完整 handoff 结构。
+ reconcile = section_text(
+ path=path,
+ section_name="full_document_reconcile",
+ ).splitlines()
+ for relative_path in entry["owned"]:
+ matches = [
+ index
+ for index, line in enumerate(reconcile)
+ if f"`{relative_path}`" in line
+ ]
+ if len(matches) != 1:
+ raise AssertionError(f"reconcile row mismatch: {relative_path}")
+ values = (
+ "## Full Document Reconcile", "none", "待判断", r"literal \| pipe",
+ "none",
+ )
+ for value in values:
+ reconcile[matches[0]] = reconcile[matches[0]].replace(
+ "待补充", value, 1,
+ )
+ replace_section(
+ path=path,
+ section_name="full_document_reconcile",
+ text="\n".join(reconcile),
+ )
+ title = entry["title"].split(maxsplit=1)[1]
+ evidence = section_text(
+ path=path,
+ section_name="agent_execution_evidence",
+ ).splitlines()
+ matches = [
+ index for index, line in enumerate(evidence)
+ if f"| {title} |" in line
+ ]
+ if len(matches) != 1:
+ raise AssertionError(f"execution row mismatch: {mode}")
+ for value in ("## Agent Execution Evidence", "none", r"x \| y"):
+ evidence[matches[0]] = evidence[matches[0]].replace(
+ "待补充", value, 1,
+ )
+ replace_section(
+ path=path,
+ section_name="agent_execution_evidence",
+ text="\n".join(evidence),
+ )
+
+
def complete_prepare(
*,
target: Path,
@@ -414,9 +526,9 @@ def complete_pass(
upstream: Path,
upstream_sha: str,
mode: str,
- relative_path: str,
+ relative_paths: list[str],
) -> None:
- """按 start/semantic edit/finish 顺序完成一个 PASS。"""
+ """按 start、逐行 body/owned docs 修改、finish 完成一个 PASS。"""
start = run_harness(
command="start-pass",
target=target,
@@ -426,10 +538,12 @@ def complete_pass(
)
if start.returncode != 0:
raise AssertionError(start.stdout + start.stderr)
- append_text(
- path=target / relative_path,
- text=f"\n{mode} semantic edit\n",
- )
+ complete_pass_body(path=target / "PR_BODY.md", mode=mode)
+ for relative_path in relative_paths:
+ append_text(
+ path=target / relative_path,
+ text=f"\n{mode} semantic edit\n",
+ )
finish = run_harness(
command="finish-pass",
target=target,
@@ -454,18 +568,22 @@ def complete_prepare_and_passes(
upstream_sha=upstream_sha,
)
edits = {
- "PASS_1": "architecture.md",
- "PASS_2": "interact.md",
- "PASS_3": "TESTING.md",
- "PASS_4": "AGENTS.md",
+ "PASS_1": ["architecture.md"],
+ "PASS_2": [
+ "capability_contract.json",
+ "interact.md",
+ "docs/business_user_guide.md",
+ ],
+ "PASS_3": ["TESTING.md"],
+ "PASS_4": ["PR_Checklist.md", "SOP.md", "AGENTS.md"],
}
- for mode, relative_path in edits.items():
+ for mode, relative_paths in edits.items():
complete_pass(
target=target,
upstream=upstream,
upstream_sha=upstream_sha,
mode=mode,
- relative_path=relative_path,
+ relative_paths=relative_paths,
)
@@ -475,56 +593,24 @@ def complete_real_prepare_and_passes(
upstream: Path,
upstream_sha: str,
) -> None:
- """用真实 skeleton/sync 完成 PREPARE 和四个 ownership PASS。"""
+ """用正式 body 保留全表结构并逐 PASS 完成已有责任行。"""
complete_prepare(
target=target,
upstream=upstream,
upstream_sha=upstream_sha,
)
- start = run_harness(
- command="start-pass",
- target=target,
- upstream=upstream,
- upstream_sha=upstream_sha,
- mode="PASS_1",
- )
- if start.returncode != 0:
- raise AssertionError(start.stdout + start.stderr)
- shutil.copy2(
- target / ".coding_workflow/diffs/pr_body_skeleton.md",
- target / "PR_BODY.md",
- )
- for section_name in (
- "repo_facts_map",
- "full_document_reconcile",
- "agent_execution_evidence",
- ):
- replace_section(
- path=target / "PR_BODY.md",
- section_name=section_name,
- text=f"## {section_name}\n\nreal PASS evidence",
- )
- append_text(path=target / "architecture.md", text="\nPASS_1 edit\n")
- finish = run_harness(
- command="finish-pass",
- target=target,
- upstream=upstream,
- upstream_sha=upstream_sha,
- mode="PASS_1",
- )
- if finish.returncode != 0:
- raise AssertionError(finish.stdout + finish.stderr)
- for mode, relative_path in {
- "PASS_2": "interact.md",
- "PASS_3": "TESTING.md",
- "PASS_4": "AGENTS.md",
+ for mode, relative_paths in {
+ "PASS_1": ["architecture.md"],
+ "PASS_2": ["interact.md"],
+ "PASS_3": ["TESTING.md"],
+ "PASS_4": ["AGENTS.md"],
}.items():
complete_pass(
target=target,
upstream=upstream,
upstream_sha=upstream_sha,
mode=mode,
- relative_path=relative_path,
+ relative_paths=relative_paths,
)
@@ -653,6 +739,10 @@ def test_titles_ownership_and_transport_alignment(self) -> None:
)
}
self.assertEqual(set(ownership), set(expected))
+ self.assertEqual(
+ HARNESS_MODULE.PERMITTED_INHERIT_PATHS,
+ set(SYNC_MODULE.PERMITTED_INHERIT_FILES),
+ )
operations = OPERATIONS.read_text(encoding="utf-8")
for mode in PASS_MODES:
self.assertEqual(ownership[mode]["title"], expected[mode]["title"])
@@ -745,16 +835,13 @@ def test_prepare_requires_clean_pinned_upstream(self) -> None:
)
self.assertNotEqual(dirty_upstream.returncode, 0)
- def test_prepare_installs_missing_templates(self) -> None:
- """真实 sync 安装模板,PASS 可从 skeleton 初始化 PR body。"""
+ def test_prepare_creates_body_and_pass_one_retries_in_place(self) -> None:
+ """Case A 各失败点保留 active PASS,补全责任项后原地成功。"""
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
upstream = root / "upstream"
target = root / "target"
- sha = create_upstream(
- repo_root=upstream,
- fake_sync=False,
- )
+ sha = create_upstream(repo_root=upstream, fake_sync=False)
initialize_repo(repo_root=target)
result = run_harness(
command="prepare",
@@ -771,6 +858,18 @@ def test_prepare_installs_missing_templates(self) -> None:
/ ".coding_workflow/skill_results/PREPARE.json"
).is_file()
)
+ body_path = target / "PR_BODY.md"
+ skeleton = target / ".coding_workflow/diffs/pr_body_skeleton.md"
+ self.assertTrue(body_path.is_file())
+ self.assertEqual(
+ body_path.read_text(encoding="utf-8"),
+ skeleton.read_text(encoding="utf-8"),
+ )
+ body = body_path.read_text(encoding="utf-8")
+ self.assertTrue(all(
+ body.count(f"") == 1
+ for name in AGENT_SECTIONS
+ ))
start = run_harness(
command="start-pass",
target=target,
@@ -779,27 +878,274 @@ def test_prepare_installs_missing_templates(self) -> None:
mode="PASS_1",
)
self.assertEqual(start.returncode, 0, msg=start.stdout)
- shutil.copy2(
- target / ".coding_workflow/diffs/pr_body_skeleton.md",
- target / "PR_BODY.md",
+ append_text(
+ path=target / "architecture.md",
+ text="\nPASS_1 semantic edit\n",
)
+ incomplete = finish_one(target=target, upstream=upstream, sha=sha)
+ self.assertNotEqual(incomplete.returncode, 0)
+ self.assertEqual(
+ parse_json(result=incomplete)["error"],
+ "PASS PR_BODY 未完成",
+ )
+ run_path = target / ".coding_workflow/skill_runtime/run.json"
+ state = json.loads(run_path.read_text(encoding="utf-8"))
+ self.assertEqual(state["active_mode"], "PASS_1")
+ body_path.unlink()
+ deleted = finish_one(target=target, upstream=upstream, sha=sha)
+ self.assertNotEqual(deleted.returncode, 0)
+ self.assertEqual(
+ parse_json(result=deleted)["error"],
+ "PR_BODY scope 无法校验",
+ )
+ baseline = json.loads(
+ (
+ target
+ / ".coding_workflow/skill_runtime/baselines/PASS_1.json"
+ ).read_text(encoding="utf-8")
+ )
+ body_path.write_text(baseline["pr_body_text"], encoding="utf-8")
+ pending_execution = section_text(
+ path=body_path,
+ section_name="agent_execution_evidence",
+ )
+ complete_pass_body(path=body_path, mode="PASS_1")
replace_section(
- path=target / "PR_BODY.md",
- section_name="repo_facts_map",
- text="PASS_1 smoke",
+ path=body_path,
+ section_name="agent_execution_evidence",
+ text=pending_execution,
+ )
+ pending = finish_one(target=target, upstream=upstream, sha=sha)
+ self.assertNotEqual(pending.returncode, 0)
+ self.assertIn("PASS 1", parse_json(result=pending)["detail"])
+ current_state = json.loads(run_path.read_text(encoding="utf-8"))
+ self.assertEqual(current_state["active_mode"], "PASS_1")
+ self.assertFalse(
+ (
+ target / ".coding_workflow/skill_results/PASS_1.json"
+ ).exists()
+ )
+
+ complete_pass_body(path=body_path, mode="PASS_1")
+ done = body_path.read_text(encoding="utf-8")
+ fact_evidence = "证据: fixture evidence"
+ reconcile_values = (
+ r"## Full Document Reconcile | none | 待判断 | "
+ r"literal \| pipe | none"
+ )
+ execution_values = r"## Agent Execution Evidence | none | x \| y"
+ execution_header = (
+ "| pass | required files read | key facts observed | "
+ "skipped files / reason |"
+ )
+ invalid_bodies = {
+ "repo_empty": done.replace(f"{fact_evidence}\n", "", 1),
+ "evidence_empty": done.replace(fact_evidence, "证据:", 1),
+ "reconcile_empty": done.replace(
+ reconcile_values, " | | | | ", 1,
+ ),
+ "execution_empty": done.replace(
+ execution_values, " | | ", 1,
+ ),
+ "literal_pipe": done.replace(
+ r"literal \| pipe", "literal | pipe", 1,
+ ),
+ "execution_header": done.replace(
+ execution_header, "| bad | bad | bad | bad | bad |", 1,
+ ),
+ }
+ for scenario, invalid_body in invalid_bodies.items():
+ with self.subTest(completion=scenario):
+ body_path.write_text(invalid_body, encoding="utf-8")
+ invalid = finish_one(
+ target=target, upstream=upstream, sha=sha,
+ )
+ self.assertNotEqual(invalid.returncode, 0)
+ payload = parse_json(result=invalid)
+ self.assertEqual(payload["error"], "PASS PR_BODY 未完成")
+ if scenario == "literal_pipe":
+ self.assertIn(
+ "表格 cell 中的字面 | 必须写为 \\| 或改用
",
+ payload["detail"],
+ )
+ with self.assertRaises(HARNESS_MODULE.HarnessError):
+ HARNESS_MODULE.assert_remaining_decisions_complete(
+ section_text="## Remaining Human Decisions\n",
+ )
+ body_path.write_text(done, encoding="utf-8")
+ upstream_architecture = (
+ upstream / "zh/architecture.md"
+ ).read_text(encoding="utf-8")
+ (target / "architecture.md").write_bytes(
+ upstream_architecture.replace("\n", "\r\n").encode("utf-8")
+ )
+ template_copy = finish_one(
+ target=target, upstream=upstream, sha=sha
+ )
+ self.assertNotEqual(template_copy.returncode, 0)
+ self.assertEqual(
+ parse_json(result=template_copy)["error"],
+ "PASS owned 文档未项目化",
+ )
+ self.assertIn(
+ "architecture.md",
+ parse_json(result=template_copy)["detail"],
)
append_text(
path=target / "architecture.md",
- text="\nPASS_1 semantic edit\n",
+ text="\nproject architecture evidence\n",
)
- finish = run_harness(
- command="finish-pass",
- target=target,
- upstream=upstream,
- upstream_sha=sha,
- mode="PASS_1",
+ self.assertIn(
+ "| PASS 2 - Capability / User Behavior | "
+ "`capability_contract.json` | `installed_template` | 待补充",
+ body_path.read_text(encoding="utf-8"),
)
+ finish = finish_one(target=target, upstream=upstream, sha=sha)
self.assertEqual(finish.returncode, 0, msg=finish.stdout)
+ state = json.loads(run_path.read_text(encoding="utf-8"))
+ self.assertEqual(state["completed_modes"], ["PREPARE", "PASS_1"])
+ self.assertIsNone(state["active_mode"])
+ pass_result = json.loads(
+ (
+ target / ".coding_workflow/skill_results/PASS_1.json"
+ ).read_text(encoding="utf-8")
+ )
+ self.assertEqual(pass_result["status"], "passed")
+ self.assertEqual(
+ pass_result["changed_paths"],
+ ["PR_BODY.md", "architecture.md"],
+ )
+
+ def test_prepare_preserves_valid_body_and_rejects_damage(self) -> None:
+ """PREPARE 迁移合法旧 schema,并在 sync 前拒绝损坏 body。"""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ root = Path(temp_dir)
+ upstream = root / "upstream"
+ sha = create_upstream(repo_root=upstream, fake_sync=False)
+ valid = pr_body_text().replace(
+ "证据: 待补充", "证据: legacy retained", 1,
+ )
+ sections = SYNC_MODULE.preserved_agent_sections(
+ text=valid, state={},
+ )
+ blocks = {
+ name: SYNC_MODULE.wrap_agent_section(
+ section_name=name, content=sections[name],
+ )
+ for name in AGENT_SECTIONS
+ }
+ reordered = valid.replace(
+ blocks["repo_facts_map"], "", 1,
+ ).replace(
+ blocks["full_document_reconcile"],
+ blocks["repo_facts_map"], 1,
+ ).replace("", blocks["full_document_reconcile"], 1)
+ last_heading = SYNC_MODULE.REPO_FACTS_HEADINGS[-1]
+ old_repo = valid.replace(
+ f"{last_heading}\n证据: 待补充", "", 1,
+ )
+ successes = {
+ "valid": valid,
+ "missing_one": old_repo.replace(
+ blocks["pr_test_evidence"], "",
+ ),
+ "missing_two": valid.replace(
+ blocks["pr_test_evidence"], "",
+ ).replace(blocks["upstream_drift_log"], ""),
+ "reordered": reordered,
+ }
+ for scenario, old_body in successes.items():
+ with self.subTest(migration=scenario):
+ target = root / scenario
+ initialize_core_target(target=target, upstream=upstream)
+ body_path = target / "PR_BODY.md"
+ body_path.write_text(old_body, encoding="utf-8")
+ result = run_harness(
+ command="prepare", target=target,
+ upstream=upstream, upstream_sha=sha,
+ )
+ self.assertEqual(result.returncode, 0, msg=result.stdout)
+ actual = body_path.read_text(encoding="utf-8")
+ HARNESS_MODULE.parse_agent_sections(text=actual)
+ preserve = SYNC_MODULE.preserved_agent_sections
+ self.assertEqual(
+ preserve(text=old_body, state={}),
+ preserve(text=actual, state={}),
+ )
+
+ heading_target = root / "missing_one"
+ body_path = heading_target / "PR_BODY.md"
+ heading_body = body_path.read_text(encoding="utf-8")
+ self.assertNotIn(last_heading, heading_body)
+ started = run_harness(
+ command="start-pass", target=heading_target,
+ upstream=upstream, upstream_sha=sha, mode="PASS_1",
+ )
+ self.assertEqual(started.returncode, 0, msg=started.stdout)
+ diffs = heading_target / ".coding_workflow/diffs"
+ schema = section_text(
+ path=diffs / "pr_body_skeleton.md",
+ section_name="repo_facts_map",
+ )
+ repo_facts = section_text(
+ path=body_path, section_name="repo_facts_map",
+ )
+ addition = schema[schema.index(last_heading):]
+ replace_section(
+ path=body_path, section_name="repo_facts_map",
+ text=f"{repo_facts.rstrip()}\n\n{addition}",
+ )
+ complete_pass_body(path=body_path, mode="PASS_1")
+ append_text(
+ path=heading_target / "architecture.md",
+ text="\nproject architecture evidence\n",
+ )
+ finished = finish_one(
+ target=heading_target, upstream=upstream, sha=sha,
+ )
+ self.assertEqual(finished.returncode, 0, msg=finished.stdout)
+ repo_start = ""
+ repo_end = ""
+ full_start = ""
+ full_end = ""
+ missing = valid.replace(blocks["pr_test_evidence"], "", 1)
+ marker = SYNC_MODULE.SYNC_PR_BODY_MARKER
+ auto_end = SYNC_MODULE.SYNC_AUTO_END
+ damage = {
+ "non_sentinel": "legacy PR body\n",
+ "marker_duplicate": valid.replace(marker, marker * 2, 1),
+ "auto_partial": valid.replace(auto_end, "", 1),
+ "only_start": valid.replace(repo_end, "", 1),
+ "only_end": valid.replace(repo_start, "", 1),
+ "missing_duplicate": missing.replace(
+ blocks["full_document_reconcile"],
+ blocks["full_document_reconcile"] * 2, 1,
+ ),
+ "missing_partial": missing.replace(full_end, "", 1),
+ "overlap": valid.replace(repo_end, "", 1).replace(
+ full_start, f"{full_start}\n{repo_end}", 1,
+ ),
+ "outside": f"outside\n{valid}",
+ }
+ for scenario, damaged_body in damage.items():
+ with self.subTest(damage=scenario):
+ target = root / f"damage_{scenario}"
+ initialize_core_target(target=target, upstream=upstream)
+ body_path = target / "PR_BODY.md"
+ body_path.write_text(damaged_body, encoding="utf-8")
+ result = run_harness(
+ command="prepare", target=target,
+ upstream=upstream, upstream_sha=sha,
+ )
+ self.assertNotEqual(result.returncode, 0)
+ self.assertEqual(
+ parse_json(result=result)["error"], "已有 PR_BODY.md 无效",
+ )
+ self.assertEqual(
+ body_path.read_text(encoding="utf-8"), damaged_body,
+ )
+ run_path = target / HARNESS_MODULE.RUN_PATH
+ self.assertFalse(run_path.exists())
def test_complete_mode_sequence_handoff(self) -> None:
"""PREPARE 至 SUBMIT 起点必须允许合法累计 dirty handoff。"""
@@ -824,7 +1170,11 @@ def test_complete_mode_sequence_handoff(self) -> None:
payload = parse_json(result=submit)
self.assertEqual(
payload["allowed_commit_paths"],
- ["AGENTS.md", "TESTING.md", "architecture.md", "interact.md"],
+ [
+ "AGENTS.md", "PR_Checklist.md", "SOP.md", "TESTING.md",
+ "architecture.md", "capability_contract.json",
+ "docs/business_user_guide.md", "interact.md",
+ ],
)
def test_start_pass_rejects_inter_mode_edit(self) -> None:
diff --git a/zh/scripts/OPERATIONS.md b/zh/scripts/OPERATIONS.md
index 8b4d595..a1db771 100644
--- a/zh/scripts/OPERATIONS.md
+++ b/zh/scripts/OPERATIONS.md
@@ -62,14 +62,26 @@ Studio / 自动编排直接从 clean pinned upstream 加载 canonical skill,
Skill 模式由调用方运行单一 `harness.py`:
-- `prepare`:校验仓库和 clean pinned upstream,运行普通 sync 并初始化 run。
+- `prepare`:校验仓库和 clean pinned upstream,运行普通 sync;若根
+ `PR_BODY.md` 缺失,则从本轮 skeleton 机械复制正式 body,再初始化 run。已有合法
+ sentinel body 保留;旧草稿或损坏 sentinel body 会失败且不会被覆盖。
- `start-pass`:校验 mode 间没有新编辑,保存本 PASS baseline,输出 prompt 位置。
-- `finish-pass`:检查 owned 文件和 PR body section,运行 pinned 普通 sync并推进 mode。
+- `finish-pass`:先检查 owned 路径、PR body 授权范围、当前 PASS 既有责任行和文档
+ readiness,再运行 pinned 普通 sync 并推进 mode。
- `prepare-submit`:建立 active、unsealed SUBMIT baseline,不运行 final gate。
- `seal-submit`:限制本阶段只能填写提交测试证据,运行 pinned final gate并封存内容。
- `finish-submit`:把 sealed snapshot/body/path set 与 commit、工作区和远端 PR 绑定。
- `status`:输出 completed modes、active mode 和结果路径。
+PREPARE 成功后,根 `PR_BODY.md` 始终是正式交接产物;
+`.coding_workflow/diffs/pr_body_skeleton.md` 只是它的机械来源,不能替代正式 body。
+PASS Agent 只能填写 ownership 声明的 section,并且只能更新匹配当前 owned path 和
+当前 PASS 的既有表格行。不得用一段摘要替换整个表格,也不得删除、改写其他 PASS 行。
+
+机械 PASS completion 只证明:1. 强制产物存在;2. sentinel / heading / row 结构
+存在;3. 当前责任字段非空;4. owned 文档达到 readiness。内容真实性与证据质量仍由
+独立 reviewer 根据仓库事实判断。
+
该 harness 防善意执行者漏步骤和顺手越权,不试图抵御与它拥有相同 shell、Git 和
工作区写权限的主动恶意执行者。
@@ -97,6 +109,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
共用执行规则:
- 完整 PR body 结构、sync sentinel、Repo Facts heading 和表头,以
`.coding_workflow/diffs/pr_body_skeleton.md` 或当前 `PR_BODY.md` 为准。
+- 表格 cell 中的字面 `|` 必须写为 `\|` 或改用 `
`;只有未转义 `|` 才作为列分隔符。
- 不得手改 `` 到 `` 区域、
任何 sync sentinel、sentinel 外内容。
- 只修改本 pass 允许的文件和 agent-owned section 内容;本 pass 负责的
@@ -123,8 +136,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
必须读取:
1. `.coding_workflow/diffs/agent_workorder.md`
2. `.coding_workflow/diffs/upstream_full/architecture.md`
-3. `PR_BODY.md`;如果不存在,先用 `.coding_workflow/diffs/pr_body_skeleton.md`
- 初始化;如果 skeleton 缺失,停止并回报普通 sync 未正确生成
+3. `PR_BODY.md` 应由 PREPARE 机械创建;如缺失立即停止并回报 PREPARE 产物异常,不得自行复制 skeleton。
4. `PR_BODY.md` 的 `repo_facts_map` 和 `full_document_reconcile` 中 `architecture.md` 行
5. 当前仓库的代码入口、模块边界、数据流、状态模型、外部依赖和代码层不变量
@@ -169,6 +181,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
共用执行规则:
- 完整 PR body 结构、sync sentinel、Repo Facts heading 和表头,以
`.coding_workflow/diffs/pr_body_skeleton.md` 或当前 `PR_BODY.md` 为准。
+- 表格 cell 中的字面 `|` 必须写为 `\|` 或改用 `
`;只有未转义 `|` 才作为列分隔符。
- 不得手改 `` 到 `` 区域、
任何 sync sentinel、sentinel 外内容。
- 只修改本 pass 允许的文件和 agent-owned section 内容;本 pass 负责的
@@ -206,8 +219,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
2. `.coding_workflow/diffs/upstream_full/capability_contract.json`
3. `.coding_workflow/diffs/upstream_full/interact.md`
4. `.coding_workflow/diffs/upstream_full/docs/business_user_guide.md`
-5. `PR_BODY.md`;如果不存在,先用 `.coding_workflow/diffs/pr_body_skeleton.md`
- 初始化;如果 skeleton 缺失,停止并回报普通 sync 未正确生成
+5. `PR_BODY.md` 应由 PREPARE 机械创建;如缺失立即停止并回报 PREPARE 产物异常,不得自行复制 skeleton。
6. `PR_BODY.md` 的 `repo_facts_map` 中能力相关事实和 `full_document_reconcile`
中本 pass 三个 owned docs 行
7. `architecture.md`、`capability_contract.json`、`interact.md`、
@@ -261,6 +273,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
共用执行规则:
- 完整 PR body 结构、sync sentinel、Repo Facts heading 和表头,以
`.coding_workflow/diffs/pr_body_skeleton.md` 或当前 `PR_BODY.md` 为准。
+- 表格 cell 中的字面 `|` 必须写为 `\|` 或改用 `
`;只有未转义 `|` 才作为列分隔符。
- 不得手改 `` 到 `` 区域、
任何 sync sentinel、sentinel 外内容。
- 只修改本 pass 允许的文件和 agent-owned section 内容;本 pass 负责的
@@ -286,8 +299,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
必须读取:
1. `.coding_workflow/diffs/agent_workorder.md`
2. `.coding_workflow/diffs/upstream_full/TESTING.md`
-3. `PR_BODY.md`;如果不存在,先用 `.coding_workflow/diffs/pr_body_skeleton.md`
- 初始化;如果 skeleton 缺失,停止并回报普通 sync 未正确生成
+3. `PR_BODY.md` 应由 PREPARE 机械创建;如缺失立即停止并回报 PREPARE 产物异常,不得自行复制 skeleton。
4. `PR_BODY.md` 的 `repo_facts_map` 中测试现状相关事实和 `full_document_reconcile`
中 `TESTING.md` 行
5. `TESTING.md`、目标项目测试入口、`tests/` 清单和机械信号命令输出;只打开机械信号
@@ -342,6 +354,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
共用执行规则:
- 完整 PR body 结构、sync sentinel、Repo Facts heading 和表头,以
`.coding_workflow/diffs/pr_body_skeleton.md` 或当前 `PR_BODY.md` 为准。
+- 表格 cell 中的字面 `|` 必须写为 `\|` 或改用 `
`;只有未转义 `|` 才作为列分隔符。
- 不得手改 `` 到 `` 区域、
任何 sync sentinel、sentinel 外内容。
- 只修改本 pass 允许的文件和 agent-owned section 内容;本 pass 负责的
@@ -384,8 +397,7 @@ owned docs,并把结论写入 `PR_BODY.md` 的 agent-owned 区。
3. `.coding_workflow/diffs/upstream_full/SOP.md`
4. `.coding_workflow/diffs/upstream_full/AGENTS.md`
5. `.coding_workflow/diffs/upstream_full/.github/pull_request_template.md`
-6. `PR_BODY.md`;如果不存在,先用 `.coding_workflow/diffs/pr_body_skeleton.md`
- 初始化;如果 skeleton 缺失,停止并回报普通 sync 未正确生成
+6. `PR_BODY.md` 应由 PREPARE 机械创建;如缺失立即停止并回报 PREPARE 产物异常,不得自行复制 skeleton。
7. `PR_BODY.md` 的 `full_document_reconcile` 全表和 `remaining_human_decisions`
8. `PR_Checklist.md`、`SOP.md`、`AGENTS.md` 和 `.github/pull_request_template.md`
9. PASS 1/2/3 owned docs 仅在验证 downstream impact 闭合时定向读取相关段落;
diff --git a/zh/skills/workflow-docs-sync/evals/README.md b/zh/skills/workflow-docs-sync/evals/README.md
index ec0d3e4..0207f0b 100644
--- a/zh/skills/workflow-docs-sync/evals/README.md
+++ b/zh/skills/workflow-docs-sync/evals/README.md
@@ -32,6 +32,12 @@
选择已有可运行代码、但缺少一个或多个核心 workflow 文档的仓库。验证两条轨道能否用
代码证据补齐缺失文档,同时避免把模板占位、无证据能力或无关治理文件写入结果。
+Shadow Case A 的 Skill 轨道还必须证明:PREPARE 后根 `PR_BODY.md` 已作为正式交接
+产物存在;每个 PASS 的成功结果同时对应正式 body 中当前 PASS 的 sentinel、heading、
+表格宽度、非空责任 cell 和 owned 文档 readiness。删除 `待补充` 但留下空 block / cell
+必须失败;表格字面 `|` 必须转义为 `\|` 或改用 `
`。这些机械条件不裁定证据真实性,
+独立 reviewer 仍负责语义 verdict。仅有 skeleton 不算正式交接或 PASS success。
+
## Case B:存在 class-2 上游漂移
选择核心文档已项目化、但未吸收当前 upstream 语义更新的仓库。验证两条轨道是否都能
diff --git a/zh/skills/workflow-docs-sync/scripts/harness.py b/zh/skills/workflow-docs-sync/scripts/harness.py
index f6ee886..763c431 100644
--- a/zh/skills/workflow-docs-sync/scripts/harness.py
+++ b/zh/skills/workflow-docs-sync/scripts/harness.py
@@ -13,6 +13,7 @@
import json
import os
import re
+import shutil
import subprocess
import sys
from datetime import datetime, timezone
@@ -33,6 +34,12 @@
SKILL_ROOT = Path(__file__).resolve().parents[1]
SOURCE_METADATA_PATH = SKILL_ROOT / ".source.json"
WORKFLOW_EXTRA_PATHS = {".gitignore", "PR_BODY.md"}
+PERMITTED_INHERIT_PATHS = {".github/pull_request_template.md"}
+SYNC_PR_BODY_MARKER = ""
+SYNC_AUTO_START = ""
+SYNC_AUTO_END = ""
+UNFILLED_PLACEHOLDER = "待补充"
+LITERAL_PIPE_GUIDANCE = "表格 cell 中的字面 | 必须写为 \\| 或改用
"
AGENT_SECTIONS = (
"repo_facts_map",
"full_document_reconcile",
@@ -663,32 +670,100 @@ def require_boundary_unchanged(
)
-def parse_agent_sections(*, text: str) -> tuple[str, dict[str, str]]:
- """参数为 PR body;预期返回占位骨架和 agent section 内容。"""
- cursor = 0
- skeleton_parts: list[str] = []
- sections: dict[str, str] = {}
- for section_name in AGENT_SECTIONS:
- start_marker = f""
- end_marker = f""
- start_index = text.find(start_marker, cursor)
- if start_index < 0:
+def validated_sentinel_spans(
+ *, text: str, require_all: bool, canonical_order: bool
+) -> dict[str, tuple[int, int]]:
+ """参数为 body 与 schema 强度;预期返回合法且不重叠的 sentinel spans。"""
+ count = text.count(SYNC_PR_BODY_MARKER)
+ if count != 1:
+ raise HarnessError(
+ error="PR_BODY sync sentinel 无效",
+ detail=f"{SYNC_PR_BODY_MARKER} 匹配次数:{count}",
+ )
+ marker_start = text.index(SYNC_PR_BODY_MARKER)
+ spans = {
+ "marker": (marker_start, marker_start + len(SYNC_PR_BODY_MARKER))
+ }
+ sentinels = [("auto", SYNC_AUTO_START, SYNC_AUTO_END, True)]
+ sentinels.extend(
+ (
+ name,
+ f"",
+ f"",
+ require_all,
+ )
+ for name in AGENT_SECTIONS
+ )
+ for name, start, end, required in sentinels:
+ start_count, end_count = text.count(start), text.count(end)
+ if start_count == 0 and end_count == 0 and not required:
+ continue
+ if start_count != 1 or end_count != 1:
+ raise HarnessError(
+ error="PR_BODY sentinel 无效",
+ detail=f"{name}: start={start_count}, end={end_count}",
+ )
+ start_index, end_index = text.index(start), text.index(end)
+ if end_index < start_index:
raise HarnessError(
- error="PR_BODY agent section 缺失",
- detail=section_name,
+ error="PR_BODY sentinel 未闭合",
+ detail=name,
)
- body_start = start_index + len(start_marker)
- end_index = text.find(end_marker, body_start)
- if end_index < 0:
+ spans[name] = (start_index, end_index + len(end))
+
+ # 旧 schema 可改变 section 顺序,但任何 span 都不能覆盖另一 span。
+ ordered = sorted(spans.items(), key=lambda item: item[1])
+ cursor = 0
+ outside_parts: list[str] = []
+ for name, (start_index, end_index) in ordered:
+ if start_index < cursor:
raise HarnessError(
- error="PR_BODY agent section 未闭合",
- detail=section_name,
+ error="PR_BODY sentinel 重叠",
+ detail=name,
)
- skeleton_parts.append(text[cursor:body_start])
- skeleton_parts.append(f"")
- skeleton_parts.append(end_marker)
- sections[section_name] = text[body_start:end_index]
- cursor = end_index + len(end_marker)
+ outside_parts.append(text[cursor:start_index])
+ cursor = end_index
+ outside_parts.append(text[cursor:])
+ if "".join(outside_parts).strip():
+ raise HarnessError(
+ error="PR_BODY sentinel 外存在内容",
+ detail="请把人工内容迁移到对应 agent-owned section",
+ )
+ expected = ["marker", "repo_facts_map", "auto", *AGENT_SECTIONS[1:]]
+ if canonical_order and [name for name, _ in ordered] != expected:
+ raise HarnessError(
+ error="PR_BODY sentinel 顺序非法",
+ detail="必须使用当前 skeleton canonical 顺序",
+ )
+ return spans
+
+
+def validate_refreshable_pr_body(*, text: str) -> None:
+ """参数为 sync 前 body;预期允许完整缺失的旧 agent section。"""
+ validated_sentinel_spans(
+ text=text, require_all=False, canonical_order=False,
+ )
+
+
+def parse_agent_sections(*, text: str) -> tuple[str, dict[str, str]]:
+ """参数为 current body;预期严格校验并返回骨架和 section。"""
+ spans = validated_sentinel_spans(
+ text=text, require_all=True, canonical_order=True,
+ )
+ cursor = 0
+ skeleton_parts: list[str] = []
+ sections: dict[str, str] = {}
+ for name in AGENT_SECTIONS:
+ start = f""
+ end = f""
+ start_index, span_end = spans[name]
+ body_start = start_index + len(start)
+ body_end = span_end - len(end)
+ skeleton_parts.extend((
+ text[cursor:body_start], f"", end,
+ ))
+ sections[name] = text[body_start:body_end]
+ cursor = span_end
skeleton_parts.append(text[cursor:])
return "".join(skeleton_parts), sections
@@ -698,8 +773,8 @@ def assert_pr_body_scope(
baseline_text: str | None,
current_text: str | None,
allowed_sections: set[str],
-) -> None:
- """参数为起止 body 和允许 section;预期其他区域完全不变。"""
+) -> tuple[dict[str, str], dict[str, str]]:
+ """参数为起止 body 和允许 section;预期返回已校验的 section。"""
if baseline_text is None or current_text is None:
raise HarnessError(
error="PR_BODY scope 无法校验",
@@ -728,6 +803,261 @@ def assert_pr_body_scope(
detail=", ".join(unauthorized),
extra={"unauthorized_sections": unauthorized},
)
+ return baseline_sections, current_sections
+
+
+def ensure_prepared_pr_body(*, repo_root: Path, refreshable: bool) -> None:
+ """参数为目标仓库;预期保留合法 body 或从 sync skeleton 复制。"""
+ body_path = repo_root / "PR_BODY.md"
+ skeleton_path = repo_root / ".coding_workflow/diffs/pr_body_skeleton.md"
+ if body_path.is_file():
+ try:
+ validator = (
+ validate_refreshable_pr_body if refreshable
+ else parse_agent_sections
+ )
+ validator(text=body_path.read_text(encoding="utf-8"))
+ except HarnessError as exc:
+ raise HarnessError(
+ error="已有 PR_BODY.md 无效",
+ detail=(
+ f"{exc.error}: {exc.detail};请移开现有文件,或人工迁移"
+ "旧内容到完整 sentinel body;原文件未覆盖"
+ ),
+ ) from exc
+ return
+ if body_path.exists():
+ raise HarnessError(
+ error="已有 PR_BODY.md 无效",
+ detail="路径不是普通文件;请移开后重试,原路径未覆盖",
+ )
+ if not skeleton_path.is_file():
+ raise HarnessError(
+ error="PREPARE PR body skeleton 缺失",
+ detail=str(skeleton_path),
+ )
+ skeleton_text = skeleton_path.read_text(encoding="utf-8")
+ parse_agent_sections(text=skeleton_text)
+ shutil.copy2(src=skeleton_path, dst=body_path)
+
+
+def matching_table_rows(
+ *, text: str, key: str, prefer_code: bool
+) -> list[str]:
+ """参数为 section、稳定行键和代码格式偏好;预期返回候选表格行。"""
+ table_lines = [line for line in text.splitlines() if line.startswith("|")]
+ if prefer_code:
+ code_rows = [line for line in table_lines if f"| `{key}` |" in line]
+ if code_rows:
+ return code_rows
+ return [line for line in table_lines if f"| {key} |" in line]
+
+
+def split_table_row(*, row: str) -> list[str]:
+ """参数为 Markdown 表格行;预期仅按未转义竖线拆分 cells。"""
+ if not row.startswith("|") or not row.endswith("|") or row.endswith("\\|"):
+ return []
+ cells: list[str] = []
+ cell_start = 1
+ for index, character in enumerate(row[1:-1], start=1):
+ if character == "|" and row[index - 1] != "\\":
+ cells.append(row[cell_start:index])
+ cell_start = index + 1
+ cells.append(row[cell_start:-1])
+ return cells
+
+
+def fail_completion(*, detail: str) -> None:
+ """参数为机械 completion 失败证据;预期抛出稳定 harness 错误。"""
+ raise HarnessError(error="PASS PR_BODY 未完成", detail=detail)
+
+
+def require_current_table(
+ *, schema_text: str, current_text: str, heading: str, label: str
+) -> int:
+ """参数为 pinned/current section;预期 current 表头保持 canonical。"""
+ schema_lines = schema_text.splitlines()
+ current_lines = current_text.splitlines()
+ schema_rows = [line for line in schema_lines if line.startswith("|")]
+ if schema_lines.count(heading) != 1 or not schema_rows:
+ fail_completion(detail=f"pinned {label} heading 或表头缺失")
+ header = schema_rows[0]
+ if current_lines.count(heading) != 1 or current_lines.count(header) != 1:
+ fail_completion(detail=f"{label} 表头不符;{LITERAL_PIPE_GUIDANCE}")
+ return len(split_table_row(row=header))
+
+
+def require_completed_row(
+ *, section_text: str, key: str, label: str, prefer_code: bool,
+ expected_cells: int, owned_cells: int
+) -> None:
+ """参数为稳定行键与 schema 宽度;预期责任 cells 非空且已填写。"""
+ matches = matching_table_rows(
+ text=section_text,
+ key=key,
+ prefer_code=prefer_code,
+ )
+ if len(matches) != 1:
+ fail_completion(
+ detail=f"{label} 匹配行数必须为 1,实际 {len(matches)}",
+ )
+ cells = split_table_row(row=matches[0])
+ if len(cells) != expected_cells:
+ fail_completion(
+ detail=(
+ f"{label} cell 数应为 {expected_cells},实际 {len(cells)};"
+ f"{LITERAL_PIPE_GUIDANCE}"
+ ),
+ )
+ if any(not cell.strip() for cell in cells[-owned_cells:]):
+ fail_completion(detail=f"{label} 责任 cell 不得为空")
+ if UNFILLED_PLACEHOLDER in matches[0]:
+ fail_completion(detail=f"{label} 仍含 {UNFILLED_PLACEHOLDER}")
+
+
+def assert_repo_facts_complete(
+ *, schema_text: str, current_text: str
+) -> None:
+ """参数为 current skeleton 与 body;预期固定事实 block 证据非空。"""
+ headings = [
+ line for line in schema_text.splitlines() if line.startswith("### ")
+ ]
+ if len(headings) != 10 or len(set(headings)) != 10:
+ fail_completion(
+ detail=f"current Repo Facts heading 数应为 10,实际 {len(headings)}",
+ )
+ current_lines = current_text.splitlines()
+ if current_lines.count("## Repo Facts Map") != 1:
+ fail_completion(detail="Repo Facts Map heading 匹配次数必须为 1")
+ for heading in headings:
+ matches = [
+ index
+ for index, line in enumerate(current_lines)
+ if line == heading
+ ]
+ if len(matches) != 1:
+ fail_completion(
+ detail=f"{heading} 匹配次数必须为 1,实际 {len(matches)}",
+ )
+ block_end = next((
+ index for index in range(matches[0] + 1, len(current_lines))
+ if current_lines[index].startswith("### ")
+ ), len(current_lines))
+ block = current_lines[matches[0] + 1:block_end]
+ evidence = [line for line in block if line.startswith("证据:")]
+ if len(evidence) != 1:
+ fail_completion(
+ detail=f"{heading} 的 证据: 行必须恰好一行",
+ )
+ if not evidence[0].partition(":")[2].strip():
+ fail_completion(detail=f"{heading} 的 证据: 值不得为空")
+ if UNFILLED_PLACEHOLDER in "\n".join(block):
+ fail_completion(detail=f"{heading} block 仍含 待补充")
+
+
+def assert_remaining_decisions_complete(*, section_text: str) -> None:
+ """参数为 Remaining Human Decisions;预期 heading 后有非空 payload。"""
+ heading = "## Remaining Human Decisions"
+ lines = [line.strip() for line in section_text.splitlines()]
+ payload = [line for line in lines if line and line != heading]
+ if lines.count(heading) != 1 or not payload:
+ fail_completion(detail="Remaining Human Decisions 缺少非空 payload")
+ if UNFILLED_PLACEHOLDER in "\n".join(payload):
+ fail_completion(detail="Remaining Human Decisions 仍含 待补充")
+
+
+def assert_pass_body_completion(
+ *,
+ mode: str,
+ ownership_entry: dict[str, Any],
+ schema_sections: dict[str, str],
+ current_sections: dict[str, str],
+) -> None:
+ """参数为 PASS ownership 与 body sections;预期当前责任项机械完成。"""
+ if mode == "PASS_1":
+ assert_repo_facts_complete(
+ schema_text=schema_sections["repo_facts_map"],
+ current_text=current_sections["repo_facts_map"],
+ )
+ reconcile_width = require_current_table(
+ schema_text=schema_sections["full_document_reconcile"],
+ current_text=current_sections["full_document_reconcile"],
+ heading="## Full Document Reconcile",
+ label="Full Document Reconcile",
+ )
+ for relative_path in ownership_entry["owned"]:
+ require_completed_row(
+ section_text=current_sections["full_document_reconcile"],
+ key=relative_path,
+ label=f"{mode} reconcile `{relative_path}`",
+ prefer_code=True,
+ expected_cells=reconcile_width,
+ owned_cells=5,
+ )
+ execution_title = re.sub(
+ pattern=r"^\d+\.\d+\s+",
+ repl="",
+ string=ownership_entry["title"],
+ )
+ execution_width = require_current_table(
+ schema_text=schema_sections["agent_execution_evidence"],
+ current_text=current_sections["agent_execution_evidence"],
+ heading="## Agent Execution Evidence",
+ label="Agent Execution Evidence",
+ )
+ require_completed_row(
+ section_text=current_sections["agent_execution_evidence"],
+ key=execution_title,
+ label=f"{execution_title} execution evidence",
+ prefer_code=False,
+ expected_cells=execution_width,
+ owned_cells=3,
+ )
+ if mode == "PASS_4":
+ assert_remaining_decisions_complete(
+ section_text=current_sections["remaining_human_decisions"],
+ )
+
+
+def assert_owned_docs_ready(
+ *,
+ mode: str,
+ target_repo: Path,
+ upstream_dir: Path,
+ ownership_entry: dict[str, Any],
+) -> None:
+ """参数为 PASS 与 pinned upstream;预期 owned 文档存在且已项目化。"""
+ for relative_path in ownership_entry["owned"]:
+ target_path = target_repo / relative_path
+ if not target_path.is_file():
+ raise HarnessError(
+ error="PASS owned 文档缺失",
+ detail=relative_path,
+ extra={"owned_path": relative_path},
+ )
+ if relative_path in PERMITTED_INHERIT_PATHS:
+ continue
+ upstream_path = upstream_dir / "zh" / relative_path
+ if not upstream_path.is_file():
+ raise HarnessError(
+ error="pinned owned 模板缺失",
+ detail=relative_path,
+ extra={"owned_path": relative_path},
+ )
+ current_text = target_path.read_text(encoding="utf-8")
+ upstream_text = upstream_path.read_text(encoding="utf-8")
+ normalized_current = current_text.replace("\r\n", "\n").replace(
+ "\r", "\n"
+ )
+ normalized_upstream = upstream_text.replace("\r\n", "\n").replace(
+ "\r", "\n"
+ )
+ if normalized_current == normalized_upstream:
+ raise HarnessError(
+ error="PASS owned 文档未项目化",
+ detail=f"{mode}: {relative_path} 仍与 pinned zh 模板完全相同",
+ extra={"owned_path": relative_path},
+ )
def git_paths_between(
@@ -853,6 +1183,10 @@ def prepare(*, args: Any) -> dict[str, Any]:
detail="先完成当前 run,或显式删除 ignored runtime 后重启",
)
+ # sync 允许旧 schema migration;先只读拒绝损坏 body,避免它被自动补写。
+ if (target_repo / "PR_BODY.md").exists():
+ ensure_prepared_pr_body(repo_root=target_repo, refreshable=True)
+
# PREPARE 之前不接受混入的业务或核心文档改动。
before_dirty = assert_dirty_allowed(
repo_root=target_repo,
@@ -869,6 +1203,9 @@ def prepare(*, args: Any) -> dict[str, Any]:
final=False,
)
+ # 正式 handoff body 必须在状态快照前落盘,后续 PASS 不再依赖 skeleton fallback。
+ ensure_prepared_pr_body(repo_root=target_repo, refreshable=False)
+
# pinned sync 只能产生 workflow 管理路径。
after_dirty = assert_dirty_allowed(
repo_root=target_repo,
@@ -1055,21 +1392,34 @@ def finish_pass(*, args: Any) -> dict[str, Any]:
detail=", ".join(unauthorized),
extra={"unauthorized_paths": unauthorized},
)
- if "PR_BODY.md" in changed:
- assert_pr_body_scope(
- baseline_text=(
- baseline["pr_body_text"]
- or (
- target_repo / ".coding_workflow/diffs/pr_body_skeleton.md"
- ).read_text(encoding="utf-8")
- ),
- current_text=read_pr_body(repo_root=target_repo),
- allowed_sections=set(
- ownership[args.mode]["pr_body_sections"]
- ),
+ _, current_sections = assert_pr_body_scope(
+ baseline_text=baseline["pr_body_text"],
+ current_text=read_pr_body(repo_root=target_repo),
+ allowed_sections=set(ownership[args.mode]["pr_body_sections"]),
+ )
+ schema_path = target_repo / ".coding_workflow/diffs/pr_body_skeleton.md"
+ if not schema_path.is_file():
+ raise HarnessError(
+ error="current PR body skeleton 缺失",
+ detail=str(schema_path),
)
+ _, schema_sections = parse_agent_sections(
+ text=schema_path.read_text(encoding="utf-8"),
+ )
+ assert_pass_body_completion(
+ mode=args.mode,
+ ownership_entry=ownership[args.mode],
+ schema_sections=schema_sections,
+ current_sections=current_sections,
+ )
+ assert_owned_docs_ready(
+ mode=args.mode,
+ target_repo=target_repo,
+ upstream_dir=upstream_dir,
+ ownership_entry=ownership[args.mode],
+ )
- # ownership 通过后才运行 pinned sync,防止 sync 掩盖 Agent 越权。
+ # ownership 与 completion 全通过后才 sync,失败不会污染 auto 区或推进状态。
sync = run_pinned_sync(
target_repo=target_repo,
upstream_dir=upstream_dir,