From 5462fe3ae67377c21ff72aea76357768c043187c Mon Sep 17 00:00:00 2001 From: Renn F Date: Wed, 3 Jun 2026 20:18:47 +0200 Subject: [PATCH] fix(briefing): developer claim precondition is note(scope='note'), not 'decision' The generated session briefing told every role with i_will_work_on that note(scope='decision') is required before claiming. That is wrong: the i_will_work_on gate is journal:note_at_claim, satisfied by has_note_for_task, which queries JournalEntryType.GENERAL. Only scope='note' maps to GENERAL; scope='decision' maps to DECISION_LOG and is the PM's i_will_plan gate. Emit scope='note' on the dev-claim branch and keep scope='decision' on the i_will_plan branch. --- roboco/runtime/orchestrator.py | 2 +- .../runtime/test_briefing_verb_server_map.py | 59 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/roboco/runtime/orchestrator.py b/roboco/runtime/orchestrator.py index 06dfcce2..f811c7f1 100644 --- a/roboco/runtime/orchestrator.py +++ b/roboco/runtime/orchestrator.py @@ -2314,7 +2314,7 @@ class AgentOrchestrator: ] if "i_will_work_on" in cfg.flow_tools: preconditions.append( - "note(scope='decision') is REQUIRED before i_will_work_on —" + "note(scope='note') is REQUIRED before i_will_work_on —" " log your approach first or the claim is rejected." ) if "i_will_plan" in cfg.flow_tools: diff --git a/tests/unit/runtime/test_briefing_verb_server_map.py b/tests/unit/runtime/test_briefing_verb_server_map.py index abf489fd..92366ec1 100644 --- a/tests/unit/runtime/test_briefing_verb_server_map.py +++ b/tests/unit/runtime/test_briefing_verb_server_map.py @@ -15,6 +15,9 @@ import tempfile from pathlib import Path from unittest.mock import patch +from roboco.foundation.policy.journaling import SCOPE_TO_TYPE, Scope +from roboco.foundation.policy.tracing import Requirement +from roboco.models.base import JournalEntryType from roboco.runtime.orchestrator import AgentOrchestrator from roboco.services.gateway.role_config import get_role_config @@ -51,9 +54,39 @@ def test_developer_block_lists_git_readonly_and_optimal_servers() -> None: def test_developer_block_states_note_before_claim_precondition() -> None: + # The i_will_work_on claim gate is journal:note_at_claim, enforced via + # JournalService.has_note_for_task -> JournalEntryType.GENERAL. The only + # note scope that maps to GENERAL is scope='note' (scope='decision' maps + # to DECISION_LOG and is the PM's i_will_plan gate). The briefing MUST + # name the scope that actually satisfies the gate, not 'decision'. block = _orch()._build_verb_server_block("developer") - assert "note(scope='decision')" in block - assert "i_will_work_on" in block.split("note(scope='decision')", 1)[1] + assert "note(scope='note')" in block + assert "i_will_work_on" in block.split("note(scope='note')", 1)[1] + # The decision scope must NOT be the dev-claim precondition. + assert "note(scope='decision')" not in block + + +def test_developer_claim_precondition_scope_matches_real_gate_type() -> None: + # Validate against the REAL boundary: the JOURNAL_NOTE_AT_CLAIM requirement + # on i_will_work_on is satisfied by has_note_for_task, which queries + # JournalEntryType.GENERAL. Whatever scope the briefing emits MUST be the + # scope whose SCOPE_TO_TYPE mapping equals the type the gate checks. + assert Requirement.JOURNAL_NOTE_AT_CLAIM.value == "journal:note_at_claim" + gate_type = JournalEntryType.GENERAL # has_note_for_task checks this type + satisfying_scopes = { + scope.value for scope, jtype in SCOPE_TO_TYPE.items() if jtype is gate_type + } + assert satisfying_scopes == {Scope.NOTE.value} + + block = _orch()._build_verb_server_block("developer") + for scope_value in satisfying_scopes: + assert f"note(scope='{scope_value}')" in block + # A scope that does NOT map to the gate type must not be presented as the + # i_will_work_on precondition. + non_satisfying = {s.value for s in Scope} - satisfying_scopes + for scope_value in non_satisfying: + claim_clause = f"note(scope='{scope_value}') is REQUIRED before i_will_work_on" + assert claim_clause not in block def test_developer_block_forbids_raw_bash_http_shell_git() -> None: @@ -69,6 +102,23 @@ def test_pm_block_states_delegate_requires_nature() -> None: assert "nature" in block +def test_pm_plan_precondition_stays_decision_scope() -> None: + # The PM's i_will_plan gate is journal:decision_at_claim, enforced via + # has_decision_for_task -> JournalEntryType.DECISION_LOG. The only scope + # that maps to DECISION_LOG is scope='decision', so the i_will_plan + # precondition must keep scope='decision' (this branch is correct). + decision_scopes = { + scope.value + for scope, jtype in SCOPE_TO_TYPE.items() + if jtype is JournalEntryType.DECISION_LOG + } + assert decision_scopes == {Scope.DECISION.value} + + block = _orch()._build_verb_server_block("cell_pm") + assert "note(scope='decision')" in block + assert "i_will_plan" in block.split("note(scope='decision')", 1)[1] + + def test_qa_block_has_no_delegate_or_note_before_claim_noise() -> None: # QA has no delegate verb, so the nature precondition must not appear; # QA has no claim-with-plan verb, so note-before-claim must not appear. @@ -125,4 +175,7 @@ def test_block_is_embedded_in_written_briefing() -> None: content = Path(path).read_text() assert "roboco-flow" in content assert "roboco-do" in content - assert "note(scope='decision')" in content + # Developer briefing: the claim precondition is scope='note' (the GENERAL + # entry has_note_for_task checks), never scope='decision'. + assert "note(scope='note')" in content + assert "note(scope='decision')" not in content