"""Planning service — start_planning aggregates plan-task creation with the project's applicable Rulebook rules and a little context, so planning happens in Scribe and rules surface at the planning moment. """ from __future__ import annotations from scribe.services import notes as notes_svc from scribe.services import projects as projects_svc from scribe.services import rulebooks as rulebooks_svc PLAN_TEMPLATE = """## Goal ## Approach ## Steps - [ ] ## Verification """ async def start_planning(user_id: int, project_id: int, title: str) -> dict: """Create a plan-task seeded with the plan template and return it with the project's applicable rules + brief context. Returns: { "task": , "applicable_rules": [...], "subscribed_rulebooks": [...], "applicable_rules_truncated": bool, "project_goal": str, "open_task_count": int, } """ project = await projects_svc.get_project(user_id, project_id) if project is None: raise ValueError(f"project {project_id} not found") note = await notes_svc.create_note( user_id, title=title, body=PLAN_TEMPLATE, status="todo", task_kind="plan", project_id=project_id, ) applicable = await rulebooks_svc.get_applicable_rules( project_id=project_id, user_id=user_id, ) _, open_count = await notes_svc.list_notes( user_id, is_task=True, status="todo", project_id=project_id, limit=1, ) return { "task": note.to_dict(), "applicable_rules": applicable["rules"], "subscribed_rulebooks": applicable["subscribed_rulebooks"], "applicable_rules_truncated": applicable["truncated"], "project_rules": applicable.get("project_rules", []), "suppressed_rules": applicable.get("suppressed_rules", []), "suppressed_topics": applicable.get("suppressed_topics", []), "project_goal": getattr(project, "goal", "") or "", "open_task_count": open_count, }