|
46 | 46 | context lines is a common failure mode `git apply` doesn't forgive. Only reached |
47 | 47 | when a ``fusion_delegate`` is configured (an ``openai``-type Delegate, already |
48 | 48 | resolved by the caller) — absent that, ``solve()`` gets ``fusion_generate=None`` |
49 | | -and stops at tree-search exactly as before (honest degrade, unchanged).""" |
| 49 | +and stops at tree-search exactly as before (honest degrade, unchanged). |
| 50 | +
|
| 51 | +**``test_rung`` (operator-only diagnostic).** Verifying a specific rung — fusion |
| 52 | +especially, only otherwise reached after three cheaper rungs fail — shouldn't |
| 53 | +require contriving a task hard enough to fail its way there. ``test_rung`` runs |
| 54 | +ONE named rung once against a feature's real acceptance tests in a throwaway |
| 55 | +worktree that's ALWAYS reaped, win or lose — never promoted, no PR. Exposed via |
| 56 | +api.py's ``test-rung`` route with no ``@tool`` wrapper, so it's operator-only, |
| 57 | +not something the board's own lead agent can reach for itself.""" |
50 | 58 |
|
51 | 59 | from __future__ import annotations |
52 | 60 |
|
|
61 | 69 |
|
62 | 70 | log = logging.getLogger("protoagent.plugins.project_board") |
63 | 71 |
|
| 72 | + |
| 73 | +def resolve_delegate(name: str, expect_type: str): |
| 74 | + """Look up a live delegate by name from the delegates registry. Returns the |
| 75 | + Delegate or None (not configured / wrong type / plugin disabled). Shared by |
| 76 | + ``loop.py`` (coder/reviewer/fusion resolution in the real dispatch path) and |
| 77 | + ``api.py`` (the operator-only test-rung route) — one lookup, not two copies.""" |
| 78 | + try: |
| 79 | + from plugins.delegates.registry import DelegateRegistry |
| 80 | + from plugins.delegates.store import merged_delegates |
| 81 | + |
| 82 | + d = DelegateRegistry(merged_delegates()).get(name) |
| 83 | + except Exception: # noqa: BLE001 — delegates plugin may be disabled |
| 84 | + return None |
| 85 | + if d is None or d.type != expect_type: |
| 86 | + return None |
| 87 | + return d |
| 88 | + |
| 89 | + |
64 | 90 | # ``### path/to/file.py`` header, then a fenced block (any/no language hint) holding |
65 | 91 | # that file's COMPLETE new content. Deliberately simple/strict: a fusion completion |
66 | 92 | # that doesn't follow the format parses to no files, which just fails verify() like |
@@ -475,3 +501,85 @@ async def dispatch( |
475 | 501 | ) |
476 | 502 | result_text = f"[coder.solve rung={result.rung} gens={result.gens_spent}] {result.note}" |
477 | 503 | return canon_wt, canon_branch, result_text |
| 504 | + |
| 505 | + |
| 506 | +async def test_rung( |
| 507 | + *, |
| 508 | + rung: str, |
| 509 | + task: str, |
| 510 | + coder, |
| 511 | + repo: str, |
| 512 | + base: str, |
| 513 | + root: str, |
| 514 | + fid: str, |
| 515 | + dispatch_timeout: float | None, |
| 516 | + test_cmd: str, |
| 517 | + test_timeout: float, |
| 518 | + budget: int = 10, |
| 519 | + k: int = 3, |
| 520 | + tree_depth: int = 2, |
| 521 | + fusion_delegate=None, |
| 522 | + fusion_k: int = 2, |
| 523 | + files_to_modify: list[str] | None = None, |
| 524 | + _solve=None, |
| 525 | + _budget_cls=None, |
| 526 | + _verdict_cls=None, |
| 527 | + _fusion_dispatch=None, |
| 528 | +) -> dict: |
| 529 | + """Operator-only diagnostic (ADR 0064): run exactly ONE named rung of |
| 530 | + ``coder.solve()`` against a feature's REAL acceptance tests, in a throwaway |
| 531 | + worktree that is ALWAYS reaped afterward — never promoted, no PR opened, no |
| 532 | + board state touched. For verifying a rung actually works (especially fusion, |
| 533 | + only otherwise reached after three cheaper rungs fail) without contriving a |
| 534 | + task hard enough to fail its way there. |
| 535 | +
|
| 536 | + Deliberately separate from ``dispatch()``: that function's contract (promote |
| 537 | + the winner, raise ``SolveExhausted`` on exhaustion) is shaped for the board's |
| 538 | + real per-feature build — mixing test semantics into it would risk the real |
| 539 | + dispatch path. This is exposed to operators only via api.py's ``test-rung`` |
| 540 | + route, which carries NO ``@tool`` wrapper — the board's own lead agent has no |
| 541 | + way to call this itself (see api.py's docstring for the same boundary the |
| 542 | + plugin already draws around ``/features/{id}/cancel`` etc.).""" |
| 543 | + if _solve is not None: |
| 544 | + solve, Budget, Verdict = _solve, _budget_cls, _verdict_cls |
| 545 | + else: |
| 546 | + from plugins.coder.solve import Budget, Verdict, solve |
| 547 | + |
| 548 | + adapter = _WorktreeSolveAdapter( |
| 549 | + repo=repo, |
| 550 | + base=base, |
| 551 | + root=root, |
| 552 | + fid=f"{fid}.test", |
| 553 | + coder=coder, |
| 554 | + dispatch_timeout=dispatch_timeout, |
| 555 | + test_cmd=test_cmd, |
| 556 | + test_timeout=test_timeout, |
| 557 | + verdict_cls=Verdict, |
| 558 | + fusion_delegate=fusion_delegate, |
| 559 | + files_to_modify=files_to_modify, |
| 560 | + _fusion_dispatch=_fusion_dispatch, |
| 561 | + ) |
| 562 | + try: |
| 563 | + result = await solve( |
| 564 | + task, |
| 565 | + generate=adapter.generate, |
| 566 | + verify=adapter.verify, |
| 567 | + budget=Budget(budget), |
| 568 | + k=k, |
| 569 | + tree_depth=tree_depth, |
| 570 | + fusion_generate=adapter.generate_fusion if fusion_delegate is not None else None, |
| 571 | + fusion_k=fusion_k, |
| 572 | + force_rung=rung, |
| 573 | + ) |
| 574 | + finally: |
| 575 | + # ALWAYS reap — pass or fail, this is a diagnostic run, never a real build. |
| 576 | + for wt, branch in adapter.candidates: |
| 577 | + await worktree.remove_worktree(repo, wt, branch) |
| 578 | + return { |
| 579 | + "rung": result.rung, |
| 580 | + "passed": result.passed, |
| 581 | + "gens_spent": result.gens_spent, |
| 582 | + "candidates_tried": result.candidates_tried, |
| 583 | + "note": result.note, |
| 584 | + "verdict_output": result.verdict.output if result.verdict else "", |
| 585 | + } |
0 commit comments