From 21343dc3aa6de5c7a1a631e410857ad6264b5e5e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 14 Sep 2026 15:34:36 -0400 Subject: [PATCH] fix(plugin): the session-start Goal line cuts at a word and says where the rest is (#4036) A raw 200-char slice ended mid-word with nothing marking the cut, so a reader took half a sentence for the whole goal. _goal_line flattens the goal to one line, trims at a word break with an ellipsis, and points at enter_project(id) when it cut. Co-Authored-By: Claude Opus 5 (1M context) --- src/scribe/services/plugin_context.py | 24 +++++++++++++++++++++++- tests/test_services_plugin_context.py | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/scribe/services/plugin_context.py b/src/scribe/services/plugin_context.py index 14fe9e5..6e3ff53 100644 --- a/src/scribe/services/plugin_context.py +++ b/src/scribe/services/plugin_context.py @@ -16,6 +16,7 @@ from __future__ import annotations import logging import re +import textwrap import time @@ -41,6 +42,10 @@ _MAX_CHARS = 9000 # Max chars of a Process body to fold into the auto-surface description. _PROC_PREVIEW_CHARS = 200 +# Max chars of the project goal on the session-start Goal line. The full goal is +# one enter_project away; a cut says so rather than ending mid-word (#4036). +_GOAL_CHARS = 200 + # --- Knowledge auto-inject (Path A: per-turn awareness push) ----------------- # Per-user settings (keys live in the generic settings table). The threshold is # deliberately STRICTER than the pull-search default (embeddings @@ -2140,6 +2145,23 @@ def _stamp_line(path: str, stamped: list[dict]) -> str: +def _goal_line(goal: str, project_id: int) -> str: + """The Goal line, trimmed at a word break with a visible cut. + + A raw slice ended mid-word with nothing to say more existed, so a reader + took half a sentence for the whole goal (#4036). + """ + if not goal: + return "" + flat = " ".join(goal.split()) + if len(flat) <= _GOAL_CHARS: + return f"Goal: {flat}" + short = textwrap.shorten(flat, width=_GOAL_CHARS, placeholder="…") + if short == "…": # one unbroken word longer than the cap + short = flat[: _GOAL_CHARS - 1] + "…" + return f"Goal: {short} (full goal: `enter_project({project_id})`)" + + async def build_session_context( user_id: int, project_id: int = 0, unbound_repo: str = "" ) -> dict: @@ -2185,7 +2207,7 @@ async def build_session_context( lines += [ "", f"## Active project: {project.title} (id {project.id})", - f"Goal: {goal[:200]}" if goal else "", + _goal_line(goal, project.id), f"Open todo tasks: {open_count}", ] diff --git a/tests/test_services_plugin_context.py b/tests/test_services_plugin_context.py index 15a7af4..21f43a7 100644 --- a/tests/test_services_plugin_context.py +++ b/tests/test_services_plugin_context.py @@ -120,6 +120,32 @@ async def test_build_session_context_includes_project_when_scoped(): assert "Reflex:" not in out["context"] +def test_a_short_goal_is_shown_whole(): + from scribe.services.plugin_context import _goal_line + assert _goal_line("ship it", 2) == "Goal: ship it" + assert _goal_line("", 2) == "" + + +def test_a_long_goal_is_cut_at_a_word_and_says_where_the_rest_is(): + """#4036: a raw slice ended mid-word with nothing marking the cut, so a + reader took half a sentence for the whole goal.""" + from scribe.services.plugin_context import _GOAL_CHARS, _goal_line + goal = "make the record reach the next session\nso a solution is recalled " * 10 + line = _goal_line(goal, 2) + shown = line.removeprefix("Goal: ").split(" (full goal:")[0] + assert shown.endswith("…") and len(shown) <= _GOAL_CHARS + # The cut lands between words: what precedes the ellipsis is a whole word. + assert shown[:-1].rstrip().split()[-1] in goal.split() + assert "\n" not in line + assert line.endswith("(full goal: `enter_project(2)`)") + + +def test_an_unbroken_goal_still_shows_its_start(): + from scribe.services.plugin_context import _GOAL_CHARS, _goal_line + shown = _goal_line("x" * 500, 2).removeprefix("Goal: ").split(" (full goal:")[0] + assert shown == "x" * (_GOAL_CHARS - 1) + "…" + + @pytest.mark.asyncio async def test_build_session_context_pushes_the_projects_design_system(): """The gap this closes: a design system had no push channel, so its