mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(board): nothing_to_propose exit for Board Program explorers + PR checks on slave-based PRs (#712)
* fix(board): give Board Program explorers a nothing_to_propose exit Every propose_* verb requires at least one item, so an explorer that legitimately found nothing — Barfly with no worthwhile X conversations, Coroner with no autopsy subject — had no way to close its exploration task. It declined, called i_am_idle(), and the task stayed PENDING forever: the dispatcher re-matched it every tick and respawned the board agent (~$0.61 a spawn, ~3 per 5-minute respawn-breaker cooldown window, indefinitely), and BoardProgramEngine's one-open-cycle dedup wedged that whole program shut, since the ledger row only closes once its exploration task goes terminal. nothing_to_propose(task_id, reason) is the explicit exit. task_id is required rather than inferred: one explorer role owns several independently-cadenced programs (head_marketing owns six) and each assigns its exploration task to the same agent, so several are open at once by design and guessing "the caller's oldest" completes the WRONG cycle — stamping its reason onto an unrelated program's ledger while the task actually being worked stays wedged. Resolution validates the named task exists, carries a registered program source, is assigned to the caller, and is non-terminal, then gates on the program's declared explorer role from the registry, so a program registered later needs no edit here. The reason lands on board_program_cycles (migration 089) and renders into the next cycle's LEARN context, replacing a bare "proposed 0, approved 0" with why. That write runs in its own savepoint: it flushes on the same session as the completion, and a bare try/except around a same-session flush leaves the transaction pending-rollback, so a DB blip there would discard the completion at the post-response commit while the verb reported success. All fourteen exploration prompts offer the exit, pinned by a registry-parametrized test that fails when a future program is unwired. * ci: fire PR checks on slave-based PRs, not master alone All five gating workflows declared `pull_request: branches: [master]`, but every fleet PR targets slave — cell->root, root->slave, and the CEO's own. So `pull_request` never fired for any of them, and their only coverage was the `push` trigger, which is gated on branch PREFIX (feature/bug/chore/docs/hotfix). A branch named anything else got zero checks — not a red run, an absent one — and a PR with no required check present merges on a false green. PR #711 shipped that way on a `fix/` branch. Basing on the branch a PR merges INTO rather than what its head is named makes coverage independent of branch naming, so a non-conforming prefix can only ever cost the redundant push run, never the whole gate. The same five also omitted slave from `push` (ci.yml aside, which added it for the release gate's fail-closed CI read), so the panel suite, both CodeQL analyses, and the e2e smoke never ran on the trunk master is cut from. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""Every Board Program exploration prompt must mention nothing_to_propose —
|
||||
the explicit "this cycle found nothing worth proposing" exit. A prompt that
|
||||
never names the verb is dead code: the explorer never learns it exists, so a
|
||||
genuinely empty cycle would still call i_am_idle() on a PENDING task and wedge
|
||||
the program's LEARN dedup forever (see ContentActions.nothing_to_propose).
|
||||
|
||||
Parametrized over ``PROGRAMS`` itself (not a hardcoded list) — a newly
|
||||
registered program with no entry in ``_PROMPT_BUILDERS`` below fails loudly,
|
||||
so this test cannot silently go stale as the registry grows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from roboco.foundation.policy.board_programs import PROGRAMS
|
||||
from roboco.runtime.orchestrator import AgentOrchestrator
|
||||
|
||||
# program key -> the AgentOrchestrator prompt-builder method name. Every
|
||||
# method here accepts a bare ``task`` dict (the LEARN/evidence context
|
||||
# params all default to "").
|
||||
_PROMPT_BUILDERS: dict[str, str] = {
|
||||
"roadmap": "_build_roadmap_prompt",
|
||||
"x_feature": "_build_feature_spotlight_prompt",
|
||||
"pest_control": "_build_pest_control_prompt",
|
||||
"periscope": "_build_periscope_prompt",
|
||||
"coroner": "_build_coroner_prompt",
|
||||
"sentinel": "_build_sentinel_prompt",
|
||||
"spackle": "_build_spackle_prompt",
|
||||
"scales": "_build_scales_prompt",
|
||||
"mirror": "_build_mirror_prompt",
|
||||
"megaphone": "_build_megaphone_prompt",
|
||||
"librarian": "_build_librarian_prompt",
|
||||
"war_room": "_build_war_room_prompt",
|
||||
"barfly": "_build_barfly_prompt",
|
||||
"dogfood": "_build_dogfood_prompt",
|
||||
}
|
||||
|
||||
|
||||
def _make_orch() -> AgentOrchestrator:
|
||||
orch = AgentOrchestrator.__new__(AgentOrchestrator)
|
||||
cast("Any", orch)._pm_respawn_tracker = {}
|
||||
return orch
|
||||
|
||||
|
||||
def _bare_task(source: str) -> dict[str, Any]:
|
||||
return {
|
||||
"id": str(uuid4()),
|
||||
"status": "pending",
|
||||
"team": "board",
|
||||
"title": "exploration cycle",
|
||||
"description": "x",
|
||||
"source": source,
|
||||
"orchestration_markers": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("program_key", sorted(PROGRAMS))
|
||||
def test_every_board_program_prompt_mentions_nothing_to_propose(
|
||||
program_key: str,
|
||||
) -> None:
|
||||
method_name = _PROMPT_BUILDERS.get(program_key)
|
||||
assert method_name is not None, (
|
||||
f"board program {program_key!r} has no entry in _PROMPT_BUILDERS — "
|
||||
"wire its exploration prompt to mention nothing_to_propose() and add "
|
||||
"it here"
|
||||
)
|
||||
orch = _make_orch()
|
||||
builder = getattr(orch, method_name)
|
||||
program = PROGRAMS[program_key]
|
||||
prompt = builder(_bare_task(program.source))
|
||||
assert "nothing_to_propose" in prompt, (
|
||||
f"{method_name} never mentions nothing_to_propose() — a genuinely "
|
||||
"empty cycle has no documented exit"
|
||||
)
|
||||
|
||||
|
||||
def test_prompt_builders_cover_exactly_the_registry() -> None:
|
||||
assert set(_PROMPT_BUILDERS) == set(PROGRAMS)
|
||||
Reference in New Issue
Block a user