diff --git a/src/onboarding/buddy_persona.py b/src/onboarding/buddy_persona.py index 8894650..31a1f14 100644 --- a/src/onboarding/buddy_persona.py +++ b/src/onboarding/buddy_persona.py @@ -70,6 +70,32 @@ _CLAIM_CLAUSE = "- When the hire picks a suggested task, offer `claim_goal`.\n" +# The one question this mentor exists for, and the one it answered from the wrong +# place. "What should I work on?" is a question about *this hire*, but Starter Work +# is also a product noun -- so a mentor told to use `search_docs` for how the product +# works explains the feature, accurately, and never names a task. The ranking exists +# only behind the tool: the corpus cannot know which tasks fit this hire, and a +# summary of an earlier visit is a record of what was suggested then, not now. +_SUGGEST_CLAUSE = ( + "- When the hire asks what to work on -- in any wording, including naming " + "Starter Work or saying somebody told them to pick something up -- call " + "`get_suggested_tasks` and present what it returns. That ranking is the only " + "place these suggestions exist, so never answer this from `search_docs`, from " + "the conversation summary, or from a list you gave earlier in this visit.\n" + "- If it comes back with nothing to suggest, say so plainly and say who can put " + "work there. A task you assembled yourself is not one anybody has agreed to.\n" +) + +# Separate from the clause above because it is only true when the arrival tool is +# mounted, and because it is the specific way the answer went missing: arrival is +# read first, the turn pauses for the backend to run it, and the hop that resumes +# has already said something helpful about setup. Naming both tools in one reply is +# what stops the question being dropped on the way back. +_SUGGEST_AFTER_ARRIVAL_CLAUSE = ( + "- Reading `get_arrival_steps` first does not stand in for that call. Both " + "belong in the same reply: what is outstanding, and the tasks themselves.\n" +) + # Deliberately an *offer*, and deliberately in the conversation. There is no # separate intake mode and no questionnaire: a hire meets the mentor and, if they # want to, is placed by talking to them. The clause has to say all three of "offer, @@ -211,6 +237,13 @@ def build_persona( "own progress.\n" ) + # After the list above, which it narrows: that clause sorts tools by subject, and + # "what should I work on" belongs to no subject cleanly enough to be routed by it. + if "get_suggested_tasks" in available: + parts.append(_SUGGEST_CLAUSE) + if "get_arrival_steps" in available: + parts.append(_SUGGEST_AFTER_ARRIVAL_CLAUSE) + # The escalation offer only makes sense when the hire can actually escalate. escalation = ( "; offer `flag_to_pm` as the last resort.\n" diff --git a/tests/onboarding/test_buddy_persona.py b/tests/onboarding/test_buddy_persona.py index 457b679..6c7b561 100644 --- a/tests/onboarding/test_buddy_persona.py +++ b/tests/onboarding/test_buddy_persona.py @@ -86,6 +86,58 @@ def test_the_arrival_clause_is_absent_without_the_tool() -> None: assert "Before suggesting anything to work on" not in persona +def test_asking_what_to_work_on_is_routed_to_the_suggestion_tool() -> None: + """The bug this clause exists for: the mentor answered the question without ever + naming a task, because the hire-state list sorts tools by subject and "what + should I work on" is not cleanly the hire's own progress.""" + persona = build_persona(_ALL_TOOLS) + + assert "When the hire asks what to work on" in persona + assert "call `get_suggested_tasks` and present what it returns" in persona + + +def test_starter_work_is_never_answered_from_the_corpus() -> None: + """Starter Work is a product noun as well as this hire's queue, and + `search_docs` is described as covering how the product works -- so without this + the model explains the feature, accurately, and names no task.""" + persona = build_persona(_ALL_TOOLS) + + assert "naming Starter Work" in persona + assert "never answer this from `search_docs`" in persona + assert "from the conversation summary" in persona + + +def test_an_empty_ranking_is_reported_rather_than_filled_in() -> None: + persona = build_persona(_ALL_TOOLS) + + assert "nothing to suggest" in persona + assert "A task you assembled yourself is not one anybody has agreed to" in persona + + +def test_the_arrival_read_does_not_stand_in_for_the_suggestion() -> None: + """Arrival pauses the turn for the backend to run it, and the hop that resumes + has already said something helpful. Both tools belong in the one reply.""" + persona = build_persona(_ALL_TOOLS) + + assert "does not stand in for that call" in persona + assert "Both belong in the same reply" in persona + + +def test_without_the_arrival_tool_nothing_is_said_about_reading_it_first() -> None: + persona = build_persona([t for t in _ALL_TOOLS if t != "get_arrival_steps"]) + + assert "When the hire asks what to work on" in persona + assert "does not stand in for that call" not in persona + + +def test_the_routing_rule_is_absent_without_the_suggestion_tool() -> None: + """A role with no starter work mounted must not be told to call for it.""" + persona = build_persona([t for t in _ALL_TOOLS if t != "get_suggested_tasks"]) + + assert "get_suggested_tasks" not in persona + assert "When the hire asks what to work on" not in persona + + def test_default_vocabulary_is_the_engineering_wording() -> None: persona = build_persona(_ALL_TOOLS, DEFAULT_VOCABULARY) @@ -202,6 +254,7 @@ def test_capabilities_off_never_offers_an_escalation_or_a_claim() -> None: assert "flag_to_pm" not in persona assert "claim_goal" not in persona assert "get_arrival_steps" not in persona + assert "get_suggested_tasks" not in persona def test_team_mode_addresses_the_manager_not_a_hire() -> None: @@ -222,6 +275,10 @@ def test_team_mode_drops_every_hire_directed_clause() -> None: assert "get_competencies_to_assess" not in persona assert "record_assessment" not in persona assert "hire-state tools" not in persona + # The identity already forbids it; dropping the routing rule as well means a + # backend that one day mounts the tool still cannot hand a manager a task list. + assert "get_suggested_tasks" not in persona + assert "what to work on" not in persona def test_team_mode_states_situations_as_facts_about_the_situation() -> None: