Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions coder_seam.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,13 @@ def __init__(
# step; the slow part (the coder dispatch) still runs in parallel.
self._wt_lock = asyncio.Lock()
self._n = 0
# worktree_path -> the coder's own final reply (its clean PR summary, per
# `loop._build_prompt`'s "your FINAL message becomes the PR description"
# contract) — captured so `dispatch()` can use the WINNING candidate's real
# summary as the PR body instead of an internal rung/gens diagnostic string.
# Also what `_verify_goal`'s NO_TEST_NEEDED escape hatch reads. Fusion has no
# such reply (a plain completion, not a summary) — absent for fusion wins.
self._replies: dict[str, str] = {}

async def _new_candidate_worktree(self) -> tuple[str, str]:
self._n += 1
Expand All @@ -349,7 +356,11 @@ async def _new_candidate_worktree(self) -> tuple[str, str]:

async def generate(self, task: str, *, feedback: str | None = None) -> str:
wt, _branch = await self._new_candidate_worktree()
await worktree.dispatch_coder(self.coder, wt, _augment_prompt(task, feedback), timeout=self.dispatch_timeout)
reply = await worktree.dispatch_coder(
self.coder, wt, _augment_prompt(task, feedback), timeout=self.dispatch_timeout
)
if (reply or "").strip():
self._replies[wt] = reply
return wt

async def generate_fusion(self, task: str, *, feedback: str | None = None) -> str:
Expand Down Expand Up @@ -510,7 +521,10 @@ async def dispatch(

Returns ``(worktree, branch, result_text)`` on a passing candidate — the SAME
3-tuple shape ``_dispatch_max_mode`` returns, so the caller's downstream drive
(fixups, local gate, ``open_pr``) is unchanged. Raises :class:`SolveExhausted`
(fixups, local gate, ``open_pr``) is unchanged. ``result_text`` is the WINNING
candidate's own reply (its clean PR summary) when the ladder reached it via an
ACP rung; only a fusion win (no natural-language reply, just file content) falls
back to an internal rung/gens diagnostic string. Raises :class:`SolveExhausted`
(a capability failure) when the budget is spent with no passing candidate, after
reaping every candidate worktree it created.

Expand Down Expand Up @@ -628,7 +642,14 @@ async def dispatch(
result.gens_spent,
budget,
)
result_text = f"[coder.solve rung={result.rung} gens={result.gens_spent}] {result.note}"
# The winning candidate's own reply (its clean PR summary, per the "your FINAL
# message becomes the PR description" contract every coder dispatch is given) is
# the real result — `loop.py` uses this verbatim as the PR body, and `_verify_goal`
# reads it for the NO_TEST_NEEDED escape hatch. Only fusion (a plain completion,
# no such reply) or an unexpectedly-empty one falls back to the diagnostic string.
result_text = (
adapter._replies.get(win_wt) or f"[coder.solve rung={result.rung} gens={result.gens_spent}] {result.note}"
)
return canon_wt, canon_branch, result_text


Expand Down
2 changes: 1 addition & 1 deletion protoagent.plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id: project_board
name: Project Board (coding orchestration)
version: 0.29.1
version: 0.29.2
description: >-
A board-driven coding-orchestration plugin: a lean 6-state board (backlog → ready
→ in_progress → in_review → done, + a blocked flag) backed by **beads** (`br`), an
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "project-board"
version = "0.29.1"
version = "0.29.2"
description = "Board-driven coding-orchestration plugin for protoAgent (beads board + ACP spawn loop + planning layer + console view)."
requires-python = ">=3.11"

Expand Down
50 changes: 49 additions & 1 deletion tests/test_coder_seam.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,58 @@ async def _fake_solve(task, *, generate, verify, budget, k, tree_depth, fusion_g
assert promoted == [("/wt/feat-bd-1.g2", "feat/bd-1.g2", "bd-1")]
assert removed == ["/wt/feat-bd-1.g1"] # only the loser reaped
assert (wt, branch) == ("/wt/feat-bd-1", "feat/bd-1") # canonical name
assert "best-of-k" in result and "gens=2" in result
# The winning candidate's OWN reply is the result — not an internal rung/gens
# diagnostic string. loop.py uses this verbatim as the PR body; _verify_goal
# reads it for the NO_TEST_NEEDED escape hatch. Losing candidate g1's reply
# must NOT leak through — only g2 (the winner) is used.
assert result == "reply from /wt/feat-bd-1.g2"
assert gens == [2] # cost surfaced exactly once


async def test_dispatch_falls_back_to_a_diagnostic_string_when_the_winner_has_no_reply(monkeypatch, tmp_path):
"""A fusion win (a plain completion, not a summary) — or any candidate whose
reply somehow never got captured — has nothing human-authored to report, so
dispatch() falls back to the rung/gens diagnostic string rather than an empty
PR body."""
_, removed, promoted = _stub_worktree(monkeypatch)

async def _create_in_tmp(repo, base, cid, root):
d = tmp_path / cid
d.mkdir(parents=True, exist_ok=True)
return (str(d), f"feat/{cid}")

monkeypatch.setattr(worktree, "create_worktree", _create_in_tmp)

async def _fake_openai_dispatch(delegate, prompt, *, timeout=None):
return "### x.py\n```\nhi\n```"

async def _fake_solve(task, *, generate, verify, budget, k, tree_depth, fusion_generate=None, fusion_k=2):
c0 = await fusion_generate(task, feedback=None) # the REAL adapter.generate_fusion
return _FakeResult(solution=c0, passed=True, rung="fusion", gens_spent=1, candidates_tried=1, note="solved")

_wt, _branch, result = await dispatch(
task="do the thing",
coder=object(),
repo="/repo",
base="main",
root=".worktrees",
fid="bd-2",
dispatch_timeout=None,
test_cmd="pytest -q",
test_timeout=30,
budget=6,
k=3,
tree_depth=2,
fusion_delegate=object(),
record_gens=lambda n: None,
_solve=_fake_solve,
_budget_cls=_FakeBudget,
_verdict_cls=_FakeVerdict,
_fusion_dispatch=_fake_openai_dispatch,
)
assert result == "[coder.solve rung=fusion gens=1] solved"


async def test_dispatch_records_gens_even_on_a_single_greedy_win(monkeypatch):
_stub_worktree(monkeypatch)

Expand Down
Loading