"""Planning service — start_planning creates a MILESTONE seeded as the plan container, surfacing the project's applicable Rulebook rules at the planning moment so rules land before any work. The milestone IS the plan: its `body` holds the design/intent (Goal/Approach/ Verification), and the individual steps live as first-class child tasks (milestone_id) rather than checkboxes crammed into one body. The legacy kind=plan task is retired going forward — start_planning never creates one. """ from __future__ import annotations from scribe.services import milestones as milestones_svc from scribe.services import notes as notes_svc from scribe.services import projects as projects_svc from scribe.services import rulebooks as rulebooks_svc # The plan body template — design only. Steps are NOT checkboxes here; each # step becomes its own child task under this milestone (status, work-logs, # priority of its own). PLAN_TEMPLATE = """## Goal ## Approach ## Verification """ async def start_planning(user_id: int, project_id: int, title: str) -> dict: """Create a milestone seeded as a plan container and return it with the project's applicable rules + brief context. Returns: { "milestone": , "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") milestone = await milestones_svc.create_milestone( user_id, project_id=project_id, title=title, body=PLAN_TEMPLATE, status="active", ) 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 { "milestone": milestone.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, }