diff --git a/skills/codex-refactor-loop/scripts/phase9_router_daemon.py b/skills/codex-refactor-loop/scripts/phase9_router_daemon.py index 14efa5e9..1b2dd2fc 100644 --- a/skills/codex-refactor-loop/scripts/phase9_router_daemon.py +++ b/skills/codex-refactor-loop/scripts/phase9_router_daemon.py @@ -48,13 +48,51 @@ *LIFECYCLE_PREFIXES, ) MARKER_RE = re.compile(r"\b(?:[A-Z][A-Z0-9_]*_(?:DONE|RESOLVED|BLOCKED)|META_JUDGE_DONE):[^\s`]+") -# Refactor (iter4/skill-router-fallback-flood-fix): Old pattern: accepted any -# marker payload, treating regex fragments containing `|` / `\"` / `*` / `r+1` -# / backslashes as real markers, flooding pending-events for every old log. -# New principle: real marker prefix and suffix segments contain only -# [A-Za-z0-9_./\-] plus limited punctuation; reject other special characters as -# prompt/regex echoes (per 2026-05-26 maintainer-directive). -VALID_MARKER_PAYLOAD = re.compile(r"^[A-Z][A-Z0-9_]*:[A-Za-z0-9_./\-]+(?::[A-Za-z0-9_./\-]+)*$") + + +class Phase9MarkerGrammar: + # Refactor (iter1/issue-149): refactor helper, no behavior change outside existing routes. + # Old pattern: phase9_router_daemon marker parser 不能可靠识别含中文收敛问题/route 后缀的 judge marker → 漏派 triplet judge 与 converge round,controller 被迫 fallback 全部 dispatch(本会话持续 no-gap churn 根因)。 + # New principle: 按 .refactor-loop/runs/phase9-issue149-r2-judge.md consensus(structural):route-specific marker-grammar parser fix,正确解析所有 route marker(含中文 body),不引入 Phase9RoundProjection 抽象。使 router 对所有 glob 可见的 3/3 SOLVER_DONE triplet 与 converge 可靠 dispatch。硬约束:不重建 REFERENCE.md;refactor 注释自含 Old/New;不超范围。 + ROUTE_TOKEN = re.compile(r"^[A-Za-z0-9_./-]+$") + VERDICT_TOKEN = re.compile(r"^[A-Za-z0-9_./-]+$") + CONVERGE_RE = re.compile(r"^META_JUDGE_DONE:converge:round-(\d+)(?::.*)?$") + + @classmethod + def parse_marker_candidate(cls, text: str) -> str | None: + if cls.is_solver_done(text) or cls.parse_converge_round(text) is not None or cls.is_stalled_marker(text): + return text + if cls._is_lifecycle_or_unknown_marker(text): + return text + return None + + @classmethod + def is_solver_done(cls, marker: str) -> bool: + parts = marker.split(":", 3) + return ( + len(parts) >= 3 + and parts[0] == "SOLVER_DONE" + and parts[1] in ROLES + and bool(cls.VERDICT_TOKEN.match(parts[2])) + ) + + @classmethod + def parse_converge_round(cls, marker: str) -> int | None: + match = cls.CONVERGE_RE.match(marker) + return int(match.group(1)) if match else None + + @classmethod + def is_stalled_marker(cls, marker: str) -> bool: + return marker.startswith("META_JUDGE_DONE:escalate:stalled:") + + @classmethod + def _is_lifecycle_or_unknown_marker(cls, marker: str) -> bool: + parts = marker.split(":") + if len(parts) < 2: + return False + if not re.match(r"^[A-Z][A-Z0-9_]*(?:_DONE|_RESOLVED|_BLOCKED)?$", parts[0]): + return False + return all(cls.ROUTE_TOKEN.match(part) for part in parts[1:]) @dataclass(frozen=True) @@ -212,6 +250,9 @@ def _collect_markers(self) -> list[Marker]: return markers def _extract_marker(self, line: str) -> str | None: + # Refactor (iter1/issue-149): + # Old pattern: phase9_router_daemon marker parser 不能可靠识别含中文收敛问题/route 后缀的 judge marker → 漏派 triplet judge 与 converge round,controller 被迫 fallback 全部 dispatch(本会话持续 no-gap churn 根因)。 + # New principle: 按 .refactor-loop/runs/phase9-issue149-r2-judge.md consensus(structural):route-specific marker-grammar parser fix,正确解析所有 route marker(含中文 body),不引入 Phase9RoundProjection 抽象。使 router 对所有 glob 可见的 3/3 SOLVER_DONE triplet 与 converge 可靠 dispatch。硬约束:不重建 REFERENCE.md;refactor 注释自含 Old/New;不超范围。 stripped = line.strip().strip("`") if self._is_placeholder_or_echo(stripped): return None @@ -228,9 +269,7 @@ def _extract_marker(self, line: str) -> str | None: candidate = match.group(0).rstrip("`.,);:|\"\\") if candidate is None: return None - if not VALID_MARKER_PAYLOAD.match(candidate): - return None - return candidate + return Phase9MarkerGrammar.parse_marker_candidate(candidate) def _is_placeholder_or_echo(self, text: str) -> bool: if "<" in text and ">" in text: @@ -395,8 +434,7 @@ def _directly_handled(self, marker: Marker, ledger: set[str]) -> bool: return False def _round_from_converge(self, marker: str) -> int | None: - match = re.match(r"META_JUDGE_DONE:converge:round-(\d+):", marker) - return int(match.group(1)) if match else None + return Phase9MarkerGrammar.parse_converge_round(marker) def _stalled_predicate_holds(self, issue: str, round_no: int) -> bool: if round_no < 3: diff --git a/skills/codex-refactor-loop/scripts/test_phase9_router_daemon.py b/skills/codex-refactor-loop/scripts/test_phase9_router_daemon.py index 8952b8bc..29650d79 100644 --- a/skills/codex-refactor-loop/scripts/test_phase9_router_daemon.py +++ b/skills/codex-refactor-loop/scripts/test_phase9_router_daemon.py @@ -165,6 +165,44 @@ def test_phase9_router_solver_triplet_dispatches_meta_judge_once(self) -> None: self.assertIn(str((self.repo / ".refactor-loop" / "logs" / "phase9-issue37-r4-judge.log").resolve()), command) self.assertEqual(self.ledger_entries()[0]["key"], "37-4-judge") + def test_phase9_router_solver_triplet_accepts_non_ascii_summary(self) -> None: + # Refactor (iter1/issue-149): + # Old pattern: phase9_router_daemon marker parser 不能可靠识别含中文收敛问题/route 后缀的 judge marker → 漏派 triplet judge 与 converge round,controller 被迫 fallback 全部 dispatch(本会话持续 no-gap churn 根因)。 + # New principle: 按 .refactor-loop/runs/phase9-issue149-r2-judge.md consensus(structural):route-specific marker-grammar parser fix,正确解析所有 route marker(含中文 body),不引入 Phase9RoundProjection 抽象。使 router 对所有 glob 可见的 3/3 SOLVER_DONE triplet 与 converge 可靠 dispatch。硬约束:不重建 REFERENCE.md;refactor 注释自含 Old/New;不超范围。 + for role in ("minimal", "structural", "delete"): + self.write_log( + f"phase9-issue149-r1-{role}.log", + f"SOLVER_DONE:{role}:propose:中文摘要-继续收敛", + ) + + self.router.tick() + + self.assertEqual(len(self.commands), 1) + self.assertIn("phase9-issue149-r1-judge.log", " ".join(self.commands[0])) + self.assertEqual([entry["key"] for entry in self.ledger_entries()], ["149-1-judge"]) + + def test_phase9_router_controller_dispatched_triplet_across_ticks(self) -> None: + for role in ("minimal", "structural"): + self.write_log( + f"phase9-issue149-r2-{role}.log", + f"SOLVER_DONE:{role}:propose:中文摘要-等待第三路", + ) + + self.router.tick() + self.assertEqual(self.commands, []) + self.assertEqual(self.ledger_entries(), []) + + self.write_log( + "phase9-issue149-r2-delete.log", + "SOLVER_DONE:delete:propose:中文摘要-第三路完成", + ) + self.router.tick() + self.router.tick() + + self.assertEqual(len(self.commands), 1) + self.assertIn("phase9-issue149-r2-judge.log", " ".join(self.commands[0])) + self.assertEqual([entry["key"] for entry in self.ledger_entries()], ["149-2-judge"]) + def test_phase9_router_accepts_solver_issue_logs_for_triplet(self) -> None: for role in ("minimal", "structural", "delete"): self.write_log( @@ -238,6 +276,24 @@ def test_phase9_router_converge_dispatches_next_round_solvers(self) -> None: ["37-5-delete", "37-5-minimal", "37-5-structural"], ) + def test_phase9_router_converge_accepts_non_ascii_reason(self) -> None: + self.write_log( + "phase9-issue149-r2-judge.log", + "META_JUDGE_DONE:converge:round-3:中文收敛问题-继续三路判断", + ) + + self.router.tick() + + self.assertEqual(len(self.commands), 3) + logs = " ".join(" ".join(command) for command in self.commands) + self.assertIn("phase9-issue149-r3-minimal.log", logs) + self.assertIn("phase9-issue149-r3-structural.log", logs) + self.assertIn("phase9-issue149-r3-delete.log", logs) + self.assertEqual( + sorted(entry["key"] for entry in self.ledger_entries()), + ["149-3-delete", "149-3-minimal", "149-3-structural"], + ) + def test_phase9_router_accepts_meta_judge_issue_log_for_converge(self) -> None: self.write_log("meta-judge-issue100-r2.log", "META_JUDGE_DONE:converge:round-3:need-more") @@ -567,6 +623,7 @@ def test_phase9_router_persists_fallback_dedup_across_restart(self) -> None: def test_phase9_router_rejects_junk_markers_with_regex_special_chars(self) -> None: """Markers containing pipe/quote/backslash/template chars are prompt/regex echoes, not real markers.""" + self.write_log("phase9-issue41-r1-judge.log", "META_JUDGE_DONE:converge:round-2:中文收敛问题-合法") junk_lines = [ 'grep "META_JUDGE_DONE:converge:r+1" log', 'pattern META_JUDGE_DONE:converge:round-2:With|round-3|Choose|minimal\\""', @@ -581,7 +638,8 @@ def test_phase9_router_rejects_junk_markers_with_regex_special_chars(self) -> No self.router.tick() events = self.pending_events() - self.assertEqual(self.commands, []) + self.assertEqual(len(self.commands), 3) + self.assertTrue(all("phase9-issue41-r2-" in " ".join(command) for command in self.commands)) for forbidden_token in ("r+1", "round-3|Choose", "no-op;", "stalled:*", "CANONICAL_HUMAN_LABELS"): with self.subTest(forbidden_token=forbidden_token): self.assertNotIn( @@ -611,6 +669,17 @@ def test_phase9_router_source_does_not_introduce_forbidden_abstractions(self) -> f"phase9_router_daemon.py must not introduce forbidden boundary token: {forbidden}", ) + def test_phase9_router_marker_grammar_is_route_specific_not_ascii_payload_gate(self) -> None: + src = PHASE9_ROUTER.read_text(encoding="utf-8") + + self.assertIn("class Phase9MarkerGrammar", src) + self.assertIn("def parse_marker_candidate", src) + self.assertIn("def parse_converge_round", src) + self.assertIn("def is_stalled_marker", src) + self.assertNotIn("class Phase9RoundProjection", src) + self.assertNotIn("Phase9RoundProjection(", src) + self.assertNotIn("VALID_MARKER_PAYLOAD.match(candidate)", src) + def test_main_once_dispatches_via_temp_repo_root(self) -> None: self.solver_triplet(issue=37, round_no=4) commands: list[list[str]] = []