"""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. Steps passed up front are created in the SAME transaction as the milestone (services/record_batch.py), so the plan body and the steps can cite each other through `{{ref:N}}` placeholders instead of predicted ids (#4016). """ 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 record_batch as batch_svc from scribe.services import rulebooks as rulebooks_svc from scribe.services.record_refs import placeholder_keys # 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, body: str | None = None, steps: list[batch_svc.BatchItem] | None = None, ) -> dict: """Create a milestone seeded as a plan container and return it with the project's applicable rules + brief context. `body` replaces the seeded template; `steps` creates the step-tasks with the milestone, atomically. Returns: { "milestone": , "applicable_rules": [...], "subscribed_rulebooks": [...], "applicable_rules_truncated": bool, "project_goal": str, "open_task_count": int, "steps": [, ...], # only when steps were given } """ project = await projects_svc.get_project(user_id, project_id) if project is None: raise ValueError(f"project {project_id} not found") plan_body = body or PLAN_TEMPLATE step_notes = [] if steps: milestone, step_notes = await batch_svc.create_batch( user_id, steps, project_id=project_id, milestone=batch_svc.BatchMilestone(title=title, body=plan_body), ) else: # A placeholder with no batch behind it has nothing to resolve to, and # storing `{{ref:1}}` verbatim would be a reference to nothing. if placeholder_keys(plan_body): raise ValueError( "the plan body uses {{ref:...}} placeholders but no steps were " "given — pass steps=[...] so they have records to name" ) milestone = await milestones_svc.create_milestone( user_id, project_id=project_id, title=title, body=plan_body, 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, ) result = { "milestone": milestone.to_dict(), **rulebooks_svc.rules_payload(applicable, user_id=user_id, source="start_planning"), "project_goal": getattr(project, "goal", "") or "", "open_task_count": open_count, } if steps: result["steps"] = [n.to_dict() for n in step_notes] return result