CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / Python tests (push) Failing after 1m3s
CI & Build / Build & push image (push) Skipped
A rule's home is its scope now: a rule in a rulebook topic is global, a rule on a project applies to that project, and retrieval reads that directly (#4074). A subscription had stopped changing anything a session received; a suppression muted rules from a subscription. Operator, 2026-09-15: "we have global and project scoped rules, we don't need the subscriptions now." What goes, whole (rule 22): - Migration 0101 drops project_rulebook_subscriptions, project_rule_suppressions and project_topic_suppressions, and strips subscribe_rulebooks (and 394's leftover exclude_always_on_rulebooks) from stored inception choices. - Service, MCP and REST: subscribe/unsubscribe and the four suppress/unsuppress operations. The Subscribers checklist, the subscribe chips, the skip buttons and the Suppressed section in the rules UI. - Inception asks two questions (design system, seed Systems). create_project and decide_project_inception lose subscribe_rulebooks. - Backup v15 stops exporting the three sections; older archives still restore, the keys simply unread. Trash no longer hard-deletes suppression rows. What changes meaning: - get_applicable_rules is a project's LISTING: its own rules, plus the global rules tagged to an area it works in. Untagged global rules apply everywhere and arrive by retrieval, so they are not listed. A co_surfaces partner on a different project is not dragged in. - list_rules(project_id) lists that project's own rules. - rules_payload drops subscribed_rulebooks and suppressed_*; the handshake's brief form is project_rules alone. - using-scribe's "Where a new rule goes" and inception sections, tool docstrings and docs say global vs project. Plugin 2026.09.15.1620. Milestone 414 step 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
"""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": <milestone dict>,
|
|
"applicable_rules": [...],
|
|
"applicable_rules_truncated": bool,
|
|
"project_rules": [...],
|
|
"project_goal": str,
|
|
"open_task_count": int,
|
|
"steps": [<task dict>, ...], # 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
|