feat(inception): the doors — create_project/decide_project_inception take the decision, enter_project asks until decided, REST inception endpoints, _INSTRUCTIONS (#2882, milestone 297 step 4)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Failing after 45s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Failing after 45s
CI & Build / Build & push image (push) Skipped
- MCP create_project(..., exclude_always_on_rulebooks, subscribe_rulebooks, design_system_id (0 unstated / -1 none / n), seed_systems): any inception arg → inception.decide(via="mcp") after the create; none → undecided with an inception_hint. New decide_project_inception(project_id, …) records or re-records; nothing given = an inherit-all decision, stated. - enter_project carries `inception` ONLY for the caller's own, undecided project: inception_ask() = the project's current defaults + what to ask the operator once + the exact call (the #2683 ask shape). Absent otherwise. - REST: POST /api/projects accepts `inception` (validated before the create); POST /api/projects/<id>/inception decides/re-decides; GET …/inception/defaults is the card's payload; GET project already carries inception via to_dict. - _INSTRUCTIONS: ORIENT names the ask; START a project names the questions — never create a project bare by default (product behaviour, P#119). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from quart import Blueprint, g, jsonify, request
|
||||
|
||||
from scribe.auth import login_required, get_current_user_id
|
||||
from scribe.routes.utils import not_found, parse_pagination
|
||||
from scribe.services import inception as inception_svc
|
||||
from scribe.services.milestones import list_milestones
|
||||
from scribe.services.notes import list_notes
|
||||
from scribe.services.projects import (
|
||||
@@ -66,6 +67,15 @@ async def create_project_route():
|
||||
status = data.get("status", "active")
|
||||
if status not in ("active", "paused", "completed", "archived"):
|
||||
return jsonify({"error": "status must be 'active', 'paused', 'completed', or 'archived'"}), 400
|
||||
# The inception decision rides the create (milestone 297): the UI's
|
||||
# second step sends `inception: {choices}`; absent = undecided, and the
|
||||
# project page shows the card until it is. Validated before the create
|
||||
# so a bad decision never leaves a half-made project behind.
|
||||
inception = data.get("inception")
|
||||
if inception is not None:
|
||||
error = inception_svc.validate_inception(inception)
|
||||
if error:
|
||||
return jsonify({"error": error}), 400
|
||||
project = await create_project(
|
||||
uid,
|
||||
title=data["title"],
|
||||
@@ -74,7 +84,44 @@ async def create_project_route():
|
||||
color=data.get("color"),
|
||||
status=status,
|
||||
)
|
||||
return jsonify(project.to_dict()), 201
|
||||
out = project.to_dict()
|
||||
if inception is not None:
|
||||
try:
|
||||
decided = await inception_svc.decide(uid, project.id, choices=inception, via="ui")
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc), "project": out}), 400
|
||||
out["inception"] = decided["inception"]
|
||||
out["inception_effects"] = decided["effects"]
|
||||
return jsonify(out), 201
|
||||
|
||||
|
||||
@projects_bp.route("/<int:project_id>/inception", methods=["POST"])
|
||||
@login_required
|
||||
async def decide_inception_route(project_id: int):
|
||||
"""Record (or re-record) what a project inherits — milestone 297.
|
||||
Body: the choices object {exclude_always_on_rulebooks, subscribe_rulebooks,
|
||||
design_system_id, seed_systems}; owner-only."""
|
||||
uid = get_current_user_id()
|
||||
data = await request.get_json() or {}
|
||||
choices = data.get("choices", data)
|
||||
try:
|
||||
decided = await inception_svc.decide(uid, project_id, choices=choices, via="ui")
|
||||
except ValueError as exc:
|
||||
msg = str(exc)
|
||||
status = 404 if "not found" in msg else 400
|
||||
return jsonify({"error": msg}), status
|
||||
return jsonify({"project_id": project_id, **decided})
|
||||
|
||||
|
||||
@projects_bp.route("/<int:project_id>/inception/defaults", methods=["GET"])
|
||||
@login_required
|
||||
async def inception_defaults_route(project_id: int):
|
||||
"""What the project inherits if nobody decides — the card's payload."""
|
||||
uid = get_current_user_id()
|
||||
try:
|
||||
return jsonify(await inception_svc.current_defaults(uid, project_id))
|
||||
except ValueError:
|
||||
return not_found("Project")
|
||||
|
||||
|
||||
@projects_bp.route("/<int:project_id>", methods=["GET"])
|
||||
|
||||
Reference in New Issue
Block a user