diff --git a/src/onboarding/buddy_persona.py b/src/onboarding/buddy_persona.py index 28c6ccf..7c54bf5 100644 --- a/src/onboarding/buddy_persona.py +++ b/src/onboarding/buddy_persona.py @@ -149,6 +149,31 @@ "later, so never call it a score, a result, or final.\n" ) +# Both halves are the point. The first is routing: "how do I create a project?" is +# a question about *this app*, and the corpus -- the project's own material -- has +# never heard of its pages, so a mentor that searches it either says nothing or +# describes a screen that does not exist. The second is the constraint that makes +# the answer safe: the guide is filtered for the reader, so a page it does not list +# is one their sidebar does not have, and sending them there is a dead end. +# +# Shared by both personas: a manager asks where things are at least as often as a +# hire does, and nothing in it presumes which of the two is reading. +_APP_GUIDE_CLAUSE = ( + "- Questions about this app itself -- where a page or button is, how to do " + "something here (create a project, set up or choose a role, connect a source, " + "write arrival steps), what the page they are on is for -- are answered by " + "`get_app_guide`, never by `search_docs`: the docs describe the project the team " + "works on, not this app. Call it before answering, even when you think you know, " + 'and read "this page" or "here" as the page it says they are looking at.\n' + "- Describe only the pages and buttons the guide names, in its words, and link a " + "page as a Markdown link to its path, like [Board](/board) -- or, where the guide " + "gives a how-to its own link, to that one, which opens the right tab. " + "Buttons on the app's own pages are not the chat buttons your tools make: naming " + "the ones the guide names is fine. " + "Never send somebody to a page the guide does not list as theirs to open: when it " + "says somebody else does it, say who to ask.\n" +) + # The onboarding-path clauses. The first is mounted on the *read* tool rather than on # the actions: a mentor that knows the path exists should talk about it even where it # may not touch it, and the backend mounts that read only for a hire who has a path. @@ -536,6 +561,10 @@ def build_persona( f"process works, and the hire-state tools ({rendered}) for the hire's " "own progress.\n" ) + # Straight after the routing rule above, which it narrows: "how this product + # works" reads as including the app, and it does not. + if "get_app_guide" in available: + parts.append(_APP_GUIDE_CLAUSE) # 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. @@ -600,6 +629,8 @@ def _team_persona(available: set[str], capabilities_enabled: bool) -> str: "`search_docs` for how the project itself works. A member id the tools " "give you is how you name somebody; never guess one.\n" ) + if "get_app_guide" in available: + parts.append(_APP_GUIDE_CLAUSE) parts.append(_TEAM_FACTS_CLAUSE) if "open_area" in available: parts.append(_TEAM_AREA_CLAUSE) diff --git a/tests/onboarding/test_buddy_persona.py b/tests/onboarding/test_buddy_persona.py index af3349c..fec30f2 100644 --- a/tests/onboarding/test_buddy_persona.py +++ b/tests/onboarding/test_buddy_persona.py @@ -694,3 +694,50 @@ def test_search_only_names_grep_only_when_it_is_mounted() -> None: assert "`grep` for an exact name" in with_grep assert "`grep` for an exact name" in team assert "grep" not in without + + +def test_app_questions_are_routed_to_the_guide_not_the_corpus() -> None: + """The corpus is the project's material; it has never heard of this app's pages.""" + persona = build_persona([*_ALL_TOOLS, "get_app_guide"]) + + assert "`get_app_guide`, never by `search_docs`" in persona + assert "create a project" in persona + + +def test_the_guide_rule_forbids_sending_somebody_to_a_page_they_cannot_open() -> None: + persona = build_persona([*_ALL_TOOLS, "get_app_guide"]) + + assert "Never send somebody to a page the guide does not list" in persona + assert "say who to ask" in persona + # Root-relative, so the chat opens it in place rather than in a new tab. A page + # everybody can open: an example is a temptation, so it must never be a dead end. + assert "[Board](/board)" in persona + # A how-to's own link opens the tab the steps talk about. + assert "gives a how-to its own link, to that one" in persona + + +def test_the_guide_rule_frees_the_app_buttons_from_the_no_button_rule() -> None: + """'Never describe a button' is about chat proposals, not the app's own pages.""" + persona = build_persona([*_ALL_TOOLS, "get_app_guide"]) + + assert "Never describe a button instead of making one" in persona + assert "not the chat buttons your tools make" in persona + + +def test_the_guide_rule_follows_the_routing_rule_it_narrows() -> None: + """'How this product works' reads as including the app; the next rule says not.""" + persona = build_persona([*_ALL_TOOLS, "get_app_guide"]) + + routing = persona.index("hire-state tools") + assert routing < persona.index("`get_app_guide`, never by") + + +def test_the_guide_rule_is_absent_without_the_tool() -> None: + assert "get_app_guide" not in build_persona(_ALL_TOOLS) + assert "get_app_guide" not in build_persona(_TEAM_TOOLS, team_mode=True) + + +def test_a_manager_is_given_the_guide_rule_too() -> None: + persona = build_persona([*_TEAM_TOOLS, "get_app_guide"], team_mode=True) + + assert "`get_app_guide`, never by `search_docs`" in persona