diff --git a/src/scribe/mcp/tools/tasks.py b/src/scribe/mcp/tools/tasks.py index be75685..d3fcbb7 100644 --- a/src/scribe/mcp/tools/tasks.py +++ b/src/scribe/mcp/tools/tasks.py @@ -152,6 +152,9 @@ async def create_task( priority: One of: low, medium, high, or 'none'. Omit (empty string) to leave unset. project_id: Associate with a project (0 = no project). milestone_id: Place within a project milestone (0 = no milestone). + When the work belongs to an active plan — a milestone in + enter_project's lists or found by search(content_type= + "milestone") — pass its id, so the plan shows all of its work. parent_id: Make this a sub-task of another task (0 = top-level). tags: List of plain-string tags without # prefix. kind: 'work' (default), 'issue', or 'spike'. @@ -462,6 +465,8 @@ async def create_records( project_id: The project every record belongs to (0 = none, or taken from milestone_id). milestone_id: File every record under this existing milestone (0 = none). + This is how steps are added to a plan that already exists, + including one start_planning handed back as `existing_milestone`. force: Bypass the near-duplicate gate for the whole batch. By default the first record that near-duplicates an existing one BLOCKS the batch, and its existing id comes back so you can update it instead. @@ -553,7 +558,7 @@ async def start_planning( match = await dedup_svc.plan_gate( uid, project_id, title, dedup_svc.plan_candidate_text( - body=body, step_texts=[f"{i.title}\n{i.body or ''}" for i in items], + body=body, steps=[(i.title, i.body) for i in items], ), ) if match is not None: diff --git a/src/scribe/services/dedup.py b/src/scribe/services/dedup.py index 45d3e01..dfa0cc9 100644 --- a/src/scribe/services/dedup.py +++ b/src/scribe/services/dedup.py @@ -767,16 +767,17 @@ async def get_plan_match_threshold(user_id: int) -> float: def plan_candidate_text( description: str | None = None, body: str | None = None, - step_texts: list[str] | None = None, + steps: list[tuple[str | None, str | None]] | None = None, ) -> str: """What a plan that doesn't exist yet says about itself, for the gate. The steps belong in it: a plan passed with steps and no design is still recognisable by them, and what its steps say is most of what makes two - plans the same plan. + plans the same plan. Each step is (title, body), joined by embedding_text + like every other record that becomes embedded text (#2486). """ parts = [(description or "").strip(), (body or "").strip()] - parts += [t.strip() for t in (step_texts or [])] + parts += [embeddings_svc.embedding_text(t, b) for t, b in (steps or [])] return "\n\n".join(p for p in parts if p) diff --git a/tests/test_services_dedup.py b/tests/test_services_dedup.py index d71e89b..ae9a9e1 100644 --- a/tests/test_services_dedup.py +++ b/tests/test_services_dedup.py @@ -408,8 +408,9 @@ async def test_a_bad_threshold_setting_falls_back_to_the_default(): def test_plan_candidate_text_carries_the_steps(): - text = plan_candidate_text(description=None, body=" design ", step_texts=["Step one\n", ""]) - assert text == "design\n\nStep one" + text = plan_candidate_text(description=None, body=" design ", + steps=[("Step one", None), ("Step two", "with a body"), (None, None)]) + assert text == "design\n\nStep one\n\nStep two\nwith a body" def test_plan_match_response_points_at_adding_steps_not_a_second_plan():