From da95b23d9c9808f6281f963cee77ad7249c99fd1 Mon Sep 17 00:00:00 2001 From: TON14 Date: Thu, 20 Aug 2026 19:06:14 +0300 Subject: [PATCH 1/3] Accept a code-span-wrapped route line from the manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manager prompt displays every route inside backticks -- "then exactly one route: `Next: gui`, `Next: cli`, ..." -- and models, most reliably the smaller ones, copy that formatting into their answer. The parser stripped `*`, so a bold `**Next: cli**` was accepted, but a code span "`Next: cli`" fell through to `invalid`: the model then re-plans against feedback that repeats the same backticked notation, and entire runs burn their round budget on formatting the harness's own instruction taught the model. Observed live: a deepseek-v4-flash manager produced a full, valid plan ending in "`Next: cli`" three rounds in a row; every round was scored invalid and the run died as max_rounds_exhausted with nothing executed. Backticks now strip exactly where asterisks already did, including after the rationale delimiter is cut ("`Next: cli` — reason" leaves a closing backtick on the route half). Prose mentioning a route mid-sentence stays invalid, as does an undelimited suffix. --- src/lh_harness/role_prompts.py | 13 +++++++++++-- tests/test_role_prompts.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/lh_harness/role_prompts.py b/src/lh_harness/role_prompts.py index d3b38847..03c419ef 100644 --- a/src/lh_harness/role_prompts.py +++ b/src/lh_harness/role_prompts.py @@ -480,12 +480,21 @@ def format_audit_findings( def parse_role_manager_next_step(text: str) -> RoleNextStep: for line in str(text or "").splitlines(): - normalized = line.strip().strip("*").replace(" ", "").replace(" ", "").lower() + # `*` covers bold; the backtick covers code spans. The prompt itself + # displays every route inside backticks ("exactly one route: + # `Next: gui`, ..."), and models -- reliably the smaller ones -- copy + # that formatting into their answer, so a parser that accepts + # `**Next: cli**` but not "`Next: cli`" burns a whole round on + # formatting the harness's own instruction taught the model. + normalized = line.strip().strip("*`").replace(" ", "").replace(" ", "").lower() # Models commonly append a short rationale after the required route, # e.g. `Next: done — all constraints passed`. Treat only an explicitly # delimited suffix as commentary so prose such as `Next: done later` # remains invalid. - normalized = re.split(r"(?:—|–|--|//|#|[((])", normalized, maxsplit=1)[0] + # Strip wrappers again after cutting the rationale: in + # "`Next: cli` — reason" the closing backtick sits before the dash and + # survives the first strip. + normalized = re.split(r"(?:—|–|--|//|#|[((])", normalized, maxsplit=1)[0].strip("*`") if normalized in {"下一步:gui任务", "下一步:gui任务", "next:gui"}: return MANAGER_NEXT_GUI if normalized in {"下一步:cli任务", "下一步:cli任务", "next:cli"}: diff --git a/tests/test_role_prompts.py b/tests/test_role_prompts.py index 290980e1..68cf3346 100644 --- a/tests/test_role_prompts.py +++ b/tests/test_role_prompts.py @@ -94,6 +94,21 @@ def test_manager_route_rejects_undelimited_suffix() -> None: assert parse_role_manager_next_step("Next: done later") != MANAGER_NEXT_DONE +@pytest.mark.parametrize( + "line", + [ + # The prompt shows every route inside backticks, and models copy that + # formatting literally; bold was already accepted, so a code span must + # not be the one markdown wrapper that voids an otherwise valid route. + "`Next: cli`", + "**`Next: cli`**", + "`Next: cli` — routed to the CLI executor", + ], +) +def test_manager_route_accepts_code_span_wrapping(line: str) -> None: + assert parse_role_manager_next_step(line) == MANAGER_NEXT_CLI + + @pytest.mark.parametrize("language", ["en", "zh"]) def test_manager_prompt_exposes_total_and_remaining_round_budget(language: str) -> None: prompt = build_role_manager_prompt( From 031d70dd615c0794c375b0a42b83405354124af9 Mon Sep 17 00:00:00 2001 From: TON14 Date: Thu, 20 Aug 2026 19:03:51 +0300 Subject: [PATCH 2/3] Run the suite on both platforms in CI The PR's own history is the argument: the branch was verified green on each platform by hand, and each round of hand-verification still found something the other platform could not see (a POSIX-only test guard, a cmd.exe-only command-line limit). A matrix of ubuntu + windows at both ends of requires-python (3.10 and 3.14) makes that check automatic for every push and pull request. The suite needs no Node toolchain -- the Web bundle is a packaging artifact -- so the job is checkout, setup-python, `pip install -e ".[test]"`, pytest. The Windows symlink fixtures skip themselves on runners without SeCreateSymbolicLinkPrivilege, which is expected and green. --- .github/workflows/tests.yml | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..1727927c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,43 @@ +name: 🧪 Tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + pytest: + name: 🐍 ${{ matrix.os }} · py${{ matrix.python }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Both supported platforms, at both ends of requires-python: the + # oldest interpreter the project promises and the newest it is + # developed against. The suite needs no Node toolchain -- the Web + # bundle is a packaging artifact, not a test dependency. + os: [ubuntu-latest, windows-latest] + python: ["3.10", "3.14"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: 📦 Install with test dependencies + run: python -m pip install -e ".[test]" + + - name: 🧪 Run the suite + # The Windows symlink fixtures skip themselves when the runner lacks + # SeCreateSymbolicLinkPrivilege; those skips are expected and green. + run: python -m pytest -q From 032c7cb316825b9330546f1f448b0adbba78b024 Mon Sep 17 00:00:00 2001 From: TON14 Date: Thu, 20 Aug 2026 20:56:04 +0300 Subject: [PATCH 3/3] Run CI on ubuntu only until the Windows platform work in #57 lands The windows-latest lanes exercise platform support this branch does not carry: it is based on a main whose supervisor still calls os.killpg and whose agent stubs are #!/bin/sh scripts, so those lanes fail on known pre-existing breakage rather than on anything in this change. #57 brings the Windows support together with the full two-platform matrix; when it merges, its version of this workflow supersedes this one. --- .github/workflows/tests.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1727927c..b569087f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,11 +21,12 @@ jobs: strategy: fail-fast: false matrix: - # Both supported platforms, at both ends of requires-python: the - # oldest interpreter the project promises and the newest it is - # developed against. The suite needs no Node toolchain -- the Web - # bundle is a packaging artifact, not a test dependency. - os: [ubuntu-latest, windows-latest] + # Ubuntu only while this branch is based on a main that predates the + # Windows support in #57: the Windows lanes there exercise platform + # code this branch does not carry, and fail on main's known + # POSIX-only layers (os.killpg and friends). #57 brings the full + # ubuntu + windows matrix; on merge, its version of this file wins. + os: [ubuntu-latest] python: ["3.10", "3.14"] steps: - uses: actions/checkout@v4