From a1c014793f8802f4f3df8a462b96968909dffa77 Mon Sep 17 00:00:00 2001 From: DavidLeuter Date: Wed, 30 Sep 2026 15:17:11 +0200 Subject: [PATCH 1/4] Route questions about the app to get_app_guide "How do I create a project?" is a question about Sprintstart, not about the project the team works on, so search_docs has nothing to say about it. A clause, gated on the backend's new get_app_guide tool and shared by the hire's and the manager's persona, routes those questions to the guide, links pages by their path, and forbids sending somebody to a page the guide does not list as theirs to open. Co-Authored-By: Claude Opus 5.5 --- src/onboarding/buddy_persona.py | 28 ++++++++++++++++++++++++++ tests/onboarding/test_buddy_persona.py | 28 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/onboarding/buddy_persona.py b/src/onboarding/buddy_persona.py index 42a73d3..ce239f5 100644 --- a/src/onboarding/buddy_persona.py +++ b/src/onboarding/buddy_persona.py @@ -119,6 +119,28 @@ "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 [Team Management](/team-management). " + "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" +) + _SEARCH_ONLY_CLAUSE = ( "- This turn you have `search_docs` and nothing else: you are answering from " "the project's own material, not acting on anybody's behalf. Answer what the " @@ -250,6 +272,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. @@ -310,6 +336,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 8e6b4da..4f6ae5f 100644 --- a/tests/onboarding/test_buddy_persona.py +++ b/tests/onboarding/test_buddy_persona.py @@ -377,3 +377,31 @@ def test_the_defaults_are_todays_behaviour() -> None: _ALL_TOOLS, capabilities_enabled=True, team_mode=False ) assert build_persona(_ALL_TOOLS, DEFAULT_VOCABULARY) == build_persona(_ALL_TOOLS) + + +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. + assert "[Team Management](/team-management)" in persona + + +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 From 7f63105148ac8e5bf65679829d64530a29350d17 Mon Sep 17 00:00:00 2001 From: DavidLeuter Date: Wed, 30 Sep 2026 15:17:23 +0200 Subject: [PATCH 2/4] Format the app-guide clause Co-Authored-By: Claude Opus 5.5 --- src/onboarding/buddy_persona.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onboarding/buddy_persona.py b/src/onboarding/buddy_persona.py index ce239f5..0980c97 100644 --- a/src/onboarding/buddy_persona.py +++ b/src/onboarding/buddy_persona.py @@ -134,7 +134,7 @@ "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" + '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 [Team Management](/team-management). " "Never send somebody to a page the guide does not list as theirs to open: when it " From ed259dc11cb5713332f59dd930fbfeddb2b41996 Mon Sep 17 00:00:00 2001 From: DavidLeuter Date: Sat, 3 Oct 2026 15:50:48 +0200 Subject: [PATCH 3/4] Link a how-to's own deep link so the right tab opens Co-Authored-By: Claude Opus 5.5 --- src/onboarding/buddy_persona.py | 4 +++- tests/onboarding/test_buddy_persona.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/onboarding/buddy_persona.py b/src/onboarding/buddy_persona.py index fe1f13c..265a6bd 100644 --- a/src/onboarding/buddy_persona.py +++ b/src/onboarding/buddy_persona.py @@ -166,7 +166,9 @@ "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 [Team Management](/team-management). " + "page as a Markdown link to its path, like [Team](/team-management) -- or, where " + "the guide gives a how-to its own link, to that one, which opens the right tab: " + "[Roles](/team-management?tab=roles). " "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" ) diff --git a/tests/onboarding/test_buddy_persona.py b/tests/onboarding/test_buddy_persona.py index c8df002..d28c76f 100644 --- a/tests/onboarding/test_buddy_persona.py +++ b/tests/onboarding/test_buddy_persona.py @@ -710,7 +710,9 @@ def test_the_guide_rule_forbids_sending_somebody_to_a_page_they_cannot_open() -> 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. - assert "[Team Management](/team-management)" in persona + assert "[Team](/team-management)" in persona + # A how-to's own link opens the tab the steps talk about. + assert "[Roles](/team-management?tab=roles)" in persona def test_the_guide_rule_is_absent_without_the_tool() -> None: From 8f64d5142d1098ec6244605629e36e6e854fd110 Mon Sep 17 00:00:00 2001 From: DavidLeuter Date: Sun, 4 Oct 2026 14:10:03 +0200 Subject: [PATCH 4/4] Keep the app guide's buttons apart from the chat's own Review feedback on the app-guide clause: - The no-button rule ("never describe a button instead of making one") is about chat proposals; say so, so the model still names the app's buttons. - The example link is now a page everybody can open (/board), not a manager page every hire persona carried. - A test pins the clause right after the search_docs routing rule it narrows. Co-Authored-By: Claude Opus 5.5 --- src/onboarding/buddy_persona.py | 7 ++++--- tests/onboarding/test_buddy_persona.py | 23 ++++++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/onboarding/buddy_persona.py b/src/onboarding/buddy_persona.py index 265a6bd..7c54bf5 100644 --- a/src/onboarding/buddy_persona.py +++ b/src/onboarding/buddy_persona.py @@ -166,9 +166,10 @@ "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 [Team](/team-management) -- or, where " - "the guide gives a how-to its own link, to that one, which opens the right tab: " - "[Roles](/team-management?tab=roles). " + "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" ) diff --git a/tests/onboarding/test_buddy_persona.py b/tests/onboarding/test_buddy_persona.py index d28c76f..fec30f2 100644 --- a/tests/onboarding/test_buddy_persona.py +++ b/tests/onboarding/test_buddy_persona.py @@ -709,10 +709,27 @@ def test_the_guide_rule_forbids_sending_somebody_to_a_page_they_cannot_open() -> 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. - assert "[Team](/team-management)" 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 "[Roles](/team-management?tab=roles)" in persona + 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: