From f95bd15ff3f5ac859781632356beb60a240c3bc0 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 26 Apr 2026 10:20:01 +0800 Subject: [PATCH] feat(pmflow-os): add execution handoff contract --- backend/app/api/pmflow_os_api/__init__.py | 34 + backend/app/schemas/pmflow_os.py | 8 + .../services/pmflow_os_projection_service.py | 311 ++++++ .../tests/test_pmflow_os_projection_api.py | 296 +++++- docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md | 426 +++++++- docs/architecture/ARCHITECTURE_INVENTORY.md | 14 +- docs/architecture/CODE_REFERENCE_INDEX.md | 11 +- .../PMFLOW_OS_PRODUCT_CONTRACT.md | 40 +- frontend/src/api/pmflowOs.ts | 113 +- .../components/ExecutionBridgeCards.tsx | 79 +- .../pmflow-os/pages/ExecutionBridgePage.tsx | 965 ++++++++---------- .../src/features/pmflow-os/store.api.test.tsx | 129 +++ frontend/src/features/pmflow-os/store.tsx | 58 ++ 13 files changed, 1921 insertions(+), 563 deletions(-) diff --git a/backend/app/api/pmflow_os_api/__init__.py b/backend/app/api/pmflow_os_api/__init__.py index fcf674b5..28b838f6 100644 --- a/backend/app/api/pmflow_os_api/__init__.py +++ b/backend/app/api/pmflow_os_api/__init__.py @@ -13,6 +13,7 @@ from ...schemas.pmflow_os import ( PMFlowChangeItemDeferRequest, PMFlowChangeItemEditRequest, PMFlowChangeSetCommitRequest, + PMFlowExecutionHandoffRequest, PMFlowExtractionCreateRequest, PMFlowThreadMessageCreateRequest, ) @@ -248,3 +249,36 @@ async def get_pmflow_os_execution_summary( raise HTTPException(status_code=404, detail="project_not_found") from exc raise return success_response(payload.model_dump(mode="json")) + + +@router.post("/projects/{project_id}/execution-handoffs") +async def create_pmflow_os_execution_handoff( + project_id: int, + body: PMFlowExecutionHandoffRequest, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + tenant_id = int(current_user.tenant_id) + await _ensure_project_member_or_manager( + db, + tenant_id=tenant_id, + project_id=int(project_id), + current_user=current_user, + ) + service = PMFlowOSProjectionService(db, tenant_id=tenant_id) + try: + payload = await service.create_execution_handoff( + project_id=int(project_id), + body=body, + current_user_id=int(current_user.id), + ) + except ValueError as exc: + code = str(exc) + if code == "task_not_found": + return JSONResponse(status_code=404, content=error_response(404, "task_not_found")) + if code == "repo_binding_not_found": + return JSONResponse(status_code=422, content=error_response(422, "repo_binding_not_found")) + if code == "project_not_found": + raise HTTPException(status_code=404, detail="project_not_found") from exc + raise + return success_response(payload.model_dump(mode="json")) diff --git a/backend/app/schemas/pmflow_os.py b/backend/app/schemas/pmflow_os.py index 70e2a145..1f3e9ebd 100644 --- a/backend/app/schemas/pmflow_os.py +++ b/backend/app/schemas/pmflow_os.py @@ -12,6 +12,7 @@ PMFlowObjectStatus = Literal["draft", "confirmed", "in_progress", "blocked", "do PMFlowRiskLevel = Literal["low", "medium", "high"] PMFlowExecutionStatus = Literal["queued", "pushed", "coding", "pr_open", "testing", "synced", "failed"] PMFlowReleaseStatus = Literal["check_pending", "ready", "shipping", "live", "watch"] +PMFlowExecutionRuntimeTarget = Literal["local_bridge", "codex", "github_agent", "manual"] ProjectLoopStage = Literal["command_idle", "ai_extracting", "extraction_ready", "review_open", "writeback_success"] PMFlowChangeGroup = Literal["new_object", "field_update", "risk", "decision"] PMFlowChangeStatus = Literal["pending_review", "accepted", "edited", "deferred", "committed", "skipped"] @@ -86,6 +87,13 @@ class PMFlowChangeSetCommitRequest(BaseModel): note: Optional[str] = None +class PMFlowExecutionHandoffRequest(BaseModel): + task_id: str = Field(min_length=1) + repo_binding_id: Optional[str] = None + runtime_target: PMFlowExecutionRuntimeTarget = "local_bridge" + note: Optional[str] = Field(default=None, max_length=4000) + + class PMFlowChangeSetDetailResponse(BaseModel): changeSet: dict[str, Any] items: list[dict[str, Any]] = Field(default_factory=list) diff --git a/backend/app/services/pmflow_os_projection_service.py b/backend/app/services/pmflow_os_projection_service.py index a2c09589..e4048a7e 100644 --- a/backend/app/services/pmflow_os_projection_service.py +++ b/backend/app/services/pmflow_os_projection_service.py @@ -25,6 +25,7 @@ from ..models import ( ProjectModelBinding, PromptPackage, ReleaseValidationRun, + RepoBinding, RevenueEvent, ROISnapshot, SemanticFact, @@ -35,6 +36,7 @@ from ..models import ( from ..schemas.pmflow_os import ( ExecutionSummaryProjection, PMFlowDecision, + PMFlowExecutionHandoffRequest, PMFlowExecutionContext, PMFlowFeedEvent, PMFlowMetricSignal, @@ -51,6 +53,7 @@ from ..schemas.pmflow_os import ( ) from .ai_project_control_service import AIProjectControlService from .pmflow_change_review_service import PMFlowChangeReviewService +from ..utils.timezone import now_cn_naive class PMFlowOSProjectionService: @@ -206,6 +209,59 @@ class PMFlowOSProjectionService: warnings=warnings, ) + async def create_execution_handoff( + self, + *, + project_id: int, + body: PMFlowExecutionHandoffRequest, + current_user_id: Optional[int] = None, + ) -> ExecutionSummaryProjection: + project = await self._load_project(project_id=project_id) + task = await self._load_project_task(project_id=project_id, task_id=body.task_id) + repo = await self._load_project_repo_binding(project_id=project_id, repo_binding_id=body.repo_binding_id) + prompt_package = await self._load_active_prompt_package_for_task(project_id=project_id, task_id=int(task.id)) + if prompt_package is None: + prompt_package = await self._create_execution_prompt_package( + project=project, + task=task, + repo=repo, + runtime_target=body.runtime_target, + note=body.note, + ) + else: + payload = self._payload(prompt_package.package_payload) + payload["latest_handoff"] = self._handoff_payload( + task=task, + repo=repo, + runtime_target=body.runtime_target, + note=body.note, + prompt_package_id=int(prompt_package.id), + ) + prompt_package.package_payload = payload + + event = ProgressEvent( + tenant_id=self.tenant_id, + task_id=int(task.id), + project_id=int(project_id), + user_id=int(current_user_id) if current_user_id is not None else None, + repo_binding_id=int(repo.id) if repo is not None else None, + event_type="handoff_created", + source="pmflow_os", + derived_task_status="pushed", + summary=self._handoff_summary(task=task, repo=repo, runtime_target=body.runtime_target), + payload=self._handoff_payload( + task=task, + repo=repo, + runtime_target=body.runtime_target, + note=body.note, + prompt_package_id=int(prompt_package.id), + ), + occurred_at=now_cn_naive(), + ) + self.db.add(event) + await self.db.commit() + return await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id) + async def _load_project(self, *, project_id: int) -> Project: result = await self.db.execute( select(Project).where(and_(Project.id == int(project_id), Project.tenant_id == self.tenant_id)) @@ -232,6 +288,47 @@ class PMFlowOSProjectionService: ) return [(task, user) for task, user in result.all()] + async def _load_project_task(self, *, project_id: int, task_id: str) -> Task: + try: + normalized_task_id = int(str(task_id).strip()) + except (TypeError, ValueError) as exc: + raise ValueError("task_not_found") from exc + result = await self.db.execute( + select(Task).where( + and_( + Task.tenant_id == self.tenant_id, + Task.project_id == int(project_id), + Task.id == normalized_task_id, + ) + ) + ) + task = result.scalar_one_or_none() + if task is None: + raise ValueError("task_not_found") + return task + + async def _load_project_repo_binding(self, *, project_id: int, repo_binding_id: Optional[str]) -> Optional[RepoBinding]: + if repo_binding_id is None or str(repo_binding_id).strip() == "": + return None + try: + normalized_repo_id = int(str(repo_binding_id).strip()) + except (TypeError, ValueError) as exc: + raise ValueError("repo_binding_not_found") from exc + result = await self.db.execute( + select(RepoBinding).where( + and_( + RepoBinding.tenant_id == self.tenant_id, + RepoBinding.project_id == int(project_id), + RepoBinding.id == normalized_repo_id, + RepoBinding.status == "active", + ) + ) + ) + repo = result.scalar_one_or_none() + if repo is None: + raise ValueError("repo_binding_not_found") + return repo + async def _load_latest_prompt_package(self, *, project_id: int) -> Optional[PromptPackage]: result = await self.db.execute( select(PromptPackage) @@ -241,6 +338,73 @@ class PMFlowOSProjectionService: ) return result.scalar_one_or_none() + async def _load_active_prompt_package_for_task(self, *, project_id: int, task_id: int) -> Optional[PromptPackage]: + result = await self.db.execute( + select(PromptPackage) + .where( + and_( + PromptPackage.tenant_id == self.tenant_id, + PromptPackage.project_id == int(project_id), + PromptPackage.task_id == int(task_id), + PromptPackage.status == "active", + ) + ) + .order_by(PromptPackage.updated_at.desc(), PromptPackage.created_at.desc(), PromptPackage.id.desc()) + .limit(1) + ) + return result.scalar_one_or_none() + + async def _create_execution_prompt_package( + self, + *, + project: Project, + task: Task, + repo: Optional[RepoBinding], + runtime_target: str, + note: Optional[str], + ) -> PromptPackage: + truth_nodes = await self._load_truth_nodes(project_id=int(project.id)) + memory = await self._project_memory(project=project, truth_nodes=truth_nodes, task_ids=[int(task.id)]) + payload = self._execution_prompt_payload( + project=project, + task=task, + repo=repo, + runtime_target=runtime_target, + note=note, + memory=memory, + ) + package = PromptPackage( + tenant_id=self.tenant_id, + project_id=int(project.id), + charter_id=int(task.truth_node_id), + task_id=int(task.id), + package_kind="execution_handoff", + status="active", + package_text=self._execution_prompt_text(payload), + package_payload=payload, + generated_from={ + "source": "pmflow_os_execution_handoff", + "project_id": str(project.id), + "task_id": str(task.id), + "repo_binding_id": None if repo is None else str(repo.id), + "runtime_target": runtime_target, + "generated_at": now_cn_naive().isoformat(), + }, + ) + self.db.add(package) + await self.db.flush() + package.package_payload = { + **payload, + "latest_handoff": self._handoff_payload( + task=task, + repo=repo, + runtime_target=runtime_target, + note=note, + prompt_package_id=int(package.id), + ), + } + return package + async def _load_project_thread(self, *, project_id: int) -> tuple[Optional[PMFlowThreadModel], list[PMFlowThreadMessage]]: thread_result = await self.db.execute( select(PMFlowThreadModel) @@ -954,6 +1118,153 @@ class PMFlowOSProjectionService: "created_at": package.created_at.isoformat() if package.created_at else None, } + def _execution_prompt_payload( + self, + *, + project: Project, + task: Task, + repo: Optional[RepoBinding], + runtime_target: str, + note: Optional[str], + memory: PMFlowProjectMemory, + ) -> dict[str, Any]: + acceptance = self._task_acceptance_criteria(task) + truth_facts = memory.truthNodeFacts[:12] + semantic_facts = memory.semanticFacts[:12] + return { + "project": { + "id": str(project.id), + "name": project.title or project.name, + "goal": project.description or project.title or project.name, + }, + "task": { + "id": str(task.id), + "title": task.title, + "description": task.description or "", + "status": task.status, + "priority": task.priority, + "charter_id": str(task.truth_node_id), + "acceptance_criteria": acceptance, + }, + "repo": None + if repo is None + else { + "id": str(repo.id), + "provider": repo.provider, + "full_name": f"{repo.owner}/{repo.repo_name}", + "default_branch": repo.default_branch, + "status": repo.status, + }, + "runtime_target": runtime_target, + "note": note, + "project_memory": { + "truth_node_facts": truth_facts, + "semantic_facts": semantic_facts, + "source_summary": memory.sourceSummary, + }, + "boundary": { + "execution": "PMFlow only creates the handoff contract and progress event. Local commands must run through Local Bridge or a compatible MCP host.", + "host_config": "PMFlow backend must not write host ~/.codex/config.toml or execute local stdio commands directly.", + }, + } + + def _execution_prompt_text(self, payload: dict[str, Any]) -> str: + project = self._payload(payload.get("project")) + task = self._payload(payload.get("task")) + repo = payload.get("repo") if isinstance(payload.get("repo"), dict) else None + memory = self._payload(payload.get("project_memory")) + truth_facts = list(memory.get("truth_node_facts") or [])[:6] + semantic_facts = list(memory.get("semantic_facts") or [])[:6] + acceptance = list(task.get("acceptance_criteria") or []) + lines = [ + "# PMFlow Execution Handoff", + "", + f"Project: {project.get('name') or project.get('id')}", + f"Goal: {project.get('goal') or '未填写项目目标'}", + "", + "## Task", + f"- ID: {task.get('id')}", + f"- Title: {task.get('title')}", + f"- Description: {task.get('description') or '未填写任务描述'}", + f"- Status: {task.get('status')}", + f"- Priority: {task.get('priority')}", + "", + "## Acceptance Criteria", + ] + lines.extend([f"- {item}" for item in acceptance] or ["- 未提供验收标准;执行前需要人工补齐。"]) + lines.extend(["", "## Repository"]) + if repo is None: + lines.append("- No repo binding is available. Create a handoff draft only; do not infer repository, branch, or local path.") + else: + lines.extend( + [ + f"- Provider: {repo.get('provider')}", + f"- Repository: {repo.get('full_name')}", + f"- Default branch: {repo.get('default_branch')}", + f"- Binding status: {repo.get('status')}", + ] + ) + lines.extend( + [ + "", + "## Runtime Target", + f"- {payload.get('runtime_target')}", + ] + ) + if payload.get("note"): + lines.extend(["", "## Operator Note", str(payload.get("note"))]) + lines.extend(["", "## Project Memory"]) + if truth_facts: + lines.append("Truth Nodes:") + lines.extend( + f"- {str(item.get('title') or item.get('id'))}: {str(item.get('summary') or item.get('status') or '')}" + for item in truth_facts + if isinstance(item, dict) + ) + if semantic_facts: + lines.append("Semantic Facts:") + lines.extend( + f"- {str(item.get('namespace') or 'fact')}.{str(item.get('key') or item.get('id'))}: {str(item.get('value') or '')}" + for item in semantic_facts + if isinstance(item, dict) + ) + if not truth_facts and not semantic_facts: + lines.append("- Project Memory has no truth/semantic facts for this task yet.") + lines.extend( + [ + "", + "## Boundary", + "- Do not execute local commands from PMFlow backend.", + "- Use Local Bridge, Codex, GitHub agent, or manual runtime according to the selected target.", + ] + ) + return "\n".join(lines) + + def _handoff_payload( + self, + *, + task: Task, + repo: Optional[RepoBinding], + runtime_target: str, + note: Optional[str], + prompt_package_id: int, + ) -> dict[str, Any]: + return { + "workflow_state": "pushed", + "handoff_state": "draft" if repo is None else "ready", + "runtime_target": runtime_target, + "prompt_package_id": str(prompt_package_id), + "task_id": str(task.id), + "repo_binding_id": None if repo is None else str(repo.id), + "repository_full_name": None if repo is None else f"{repo.owner}/{repo.repo_name}", + "note": note, + "boundary": "PMFlow backend creates metadata only; execution must be picked up by Local Bridge, Codex, GitHub agent, or manual operator.", + } + + def _handoff_summary(self, *, task: Task, repo: Optional[RepoBinding], runtime_target: str) -> str: + target = f"{repo.owner}/{repo.repo_name}" if repo is not None else "未绑定仓库" + return f"执行交接已创建:{task.title} -> {runtime_target} / {target}" + def _latest_iso(self, values: list[Optional[datetime]]) -> Optional[str]: valid = [value for value in values if value is not None] if not valid: diff --git a/backend/tests/test_pmflow_os_projection_api.py b/backend/tests/test_pmflow_os_projection_api.py index 2ce6155a..f4bfc9de 100644 --- a/backend/tests/test_pmflow_os_projection_api.py +++ b/backend/tests/test_pmflow_os_projection_api.py @@ -1,13 +1,14 @@ from __future__ import annotations import os +from datetime import datetime from types import SimpleNamespace import pytest import pytest_asyncio from fastapi import FastAPI from httpx import ASGITransport, AsyncClient -from sqlalchemy import JSON +from sqlalchemy import JSON, select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test") @@ -283,6 +284,299 @@ async def test_execution_summary_empty_state_is_pending_with_warnings(db_env) -> assert payload["activeDevTasks"] == [] +@pytest.mark.asyncio +async def test_execution_summary_projects_real_prompt_repo_tasks_and_events(db_env) -> None: + session = db_env + user, project = await _seed_user_project(session) + charter = TruthNode( + tenant_id=1, + node_type="charter", + project_id=project.id, + created_by=user.id, + title="真实执行宪章", + content={"business_goal": "进入真实开发执行桥"}, + version=1, + status="aligned", + ) + session.add(charter) + await session.flush() + task = Task( + tenant_id=1, + truth_node_id=charter.id, + project_id=project.id, + assignee_id=user.id, + ai_recommended_assignees=None, + ai_breakdown_payload={"acceptance_criteria": ["必须投影真实 dev-sync 数据"]}, + manual_overrides=None, + title="接入真实执行摘要", + description="从 execution-summary 渲染执行桥", + required_skill_tag_ids=[], + status="in_progress", + priority="high", + ) + repo = RepoBinding( + tenant_id=1, + project_id=project.id, + provider="github", + owner="pmflow", + repo_name="pmflow-app", + default_branch="main", + installation_id="install-1", + status="active", + ) + session.add_all([task, repo]) + await session.flush() + session.add_all( + [ + PromptPackage( + tenant_id=1, + project_id=project.id, + charter_id=charter.id, + task_id=task.id, + package_kind="development", + status="active", + package_text="真实开发提示词", + package_payload={"scope": "execution"}, + generated_from={"source": "truth_state"}, + ), + TaskCodeLink( + tenant_id=1, + task_id=task.id, + repo_binding_id=repo.id, + branch_name="codex/main-flow-phase-d-execution", + pr_number=128, + last_commit_sha="abcdef1234567890", + status="review", + ), + ProgressEvent( + tenant_id=1, + task_id=task.id, + project_id=project.id, + user_id=user.id, + repo_binding_id=repo.id, + event_type="commit", + source="github", + derived_task_status="in_progress", + summary="真实 commit 已回流", + payload={ + "workflow_state": "coding", + "branch_name": "codex/main-flow-phase-d-execution", + "commit_sha": "abcdef1234567890", + "pr_number": 128, + }, + occurred_at=datetime(2026, 4, 26, 10, 30, 0), + ), + GitHubWebhookDelivery( + tenant_id=1, + delivery_id="delivery-1", + event_type="push", + repo_binding_id=repo.id, + project_id=project.id, + installation_id="install-1", + status="matched", + payload={"repository": {"full_name": "pmflow/pmflow-app"}}, + received_at=datetime(2026, 4, 26, 10, 31, 0), + ), + ] + ) + await session.commit() + app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER")) + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get(f"/pmflow-os/projects/{project.id}/execution-summary") + + assert response.status_code == 200 + payload = response.json()["data"] + assert payload["promptPackage"]["package_text"] == "真实开发提示词" + assert payload["repoBindings"][0]["full_name"] == "pmflow/pmflow-app" + assert payload["repoBindings"][0]["task_link_count"] == 1 + assert payload["activeDevTasks"][0]["title"] == "接入真实执行摘要" + assert payload["activeDevTasks"][0]["code_link"]["pr_number"] == 128 + assert payload["recentProgressEvents"][0]["summary"] == "真实 commit 已回流" + assert payload["recentDeliveries"][0]["repository_full_name"] == "pmflow/pmflow-app" + assert payload["warnings"] == [] + + +@pytest.mark.asyncio +async def test_execution_handoff_creates_prompt_package_and_progress_event(db_env) -> None: + session = db_env + user, project = await _seed_user_project(session) + charter = TruthNode( + tenant_id=1, + node_type="charter", + project_id=project.id, + created_by=user.id, + title="执行交接宪章", + content={"business_goal": "把确认任务交给真实开发执行桥"}, + version=1, + status="aligned", + ) + session.add(charter) + await session.flush() + task = Task( + tenant_id=1, + truth_node_id=charter.id, + project_id=project.id, + assignee_id=user.id, + ai_recommended_assignees=None, + ai_breakdown_payload={"acceptance_criteria": ["生成可追踪 handoff 起点"]}, + manual_overrides=None, + title="发起真实执行交接", + description="从任务、项目记忆和仓库生成 PromptPackage", + required_skill_tag_ids=[], + status="confirmed", + priority="high", + ) + repo = RepoBinding( + tenant_id=1, + project_id=project.id, + provider="github", + owner="pmflow", + repo_name="execution-bridge", + default_branch="main", + installation_id="install-2", + status="active", + ) + session.add_all([task, repo]) + await session.commit() + app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER")) + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.post( + f"/pmflow-os/projects/{project.id}/execution-handoffs", + json={ + "task_id": str(task.id), + "repo_binding_id": str(repo.id), + "runtime_target": "local_bridge", + "note": "交给本地桥接执行", + }, + ) + + assert response.status_code == 200 + payload = response.json()["data"] + assert payload["promptPackage"]["task_id"] == str(task.id) + assert payload["promptPackage"]["package_kind"] == "execution_handoff" + assert "发起真实执行交接" in payload["promptPackage"]["package_text"] + assert "pmflow/execution-bridge" in payload["promptPackage"]["package_text"] + assert payload["recentProgressEvents"][0]["event_type"] == "handoff_created" + assert payload["recentProgressEvents"][0]["repo"]["full_name"] == "pmflow/execution-bridge" + assert payload["execution"]["status"] == "pushed" + assert payload["warnings"] == [] + + prompt_rows = (await session.execute(select(PromptPackage).where(PromptPackage.task_id == task.id))).scalars().all() + event_rows = (await session.execute(select(ProgressEvent).where(ProgressEvent.task_id == task.id))).scalars().all() + assert len(prompt_rows) == 1 + assert len(event_rows) == 1 + assert event_rows[0].payload["runtime_target"] == "local_bridge" + + +@pytest.mark.asyncio +async def test_execution_handoff_without_repo_creates_draft_warning(db_env) -> None: + session = db_env + user, project = await _seed_user_project(session) + charter = TruthNode( + tenant_id=1, + node_type="charter", + project_id=project.id, + created_by=user.id, + title="无仓库交接宪章", + content={"business_goal": "允许先生成执行交接草案"}, + version=1, + status="aligned", + ) + session.add(charter) + await session.flush() + task = Task( + tenant_id=1, + truth_node_id=charter.id, + project_id=project.id, + assignee_id=user.id, + ai_recommended_assignees=None, + ai_breakdown_payload=None, + manual_overrides=None, + title="缺少仓库也能建草案", + description="仓库后续绑定", + required_skill_tag_ids=[], + status="confirmed", + priority="medium", + ) + session.add(task) + await session.commit() + app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER")) + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.post( + f"/pmflow-os/projects/{project.id}/execution-handoffs", + json={"task_id": str(task.id), "runtime_target": "manual"}, + ) + + assert response.status_code == 200 + payload = response.json()["data"] + assert payload["promptPackage"]["task_id"] == str(task.id) + assert payload["recentProgressEvents"][0]["repo"] is None + assert "repo_bindings_empty" in " ".join(payload["warnings"]) + event = (await session.execute(select(ProgressEvent).where(ProgressEvent.task_id == task.id))).scalar_one() + assert event.payload["handoff_state"] == "draft" + + +@pytest.mark.asyncio +async def test_execution_handoff_rejects_task_outside_project(db_env) -> None: + session = db_env + user, project = await _seed_user_project(session) + other_project = Project( + tenant_id=1, + name="其它真实项目", + title="其它真实项目", + description="不能被当前项目交接", + owner_id=user.id, + status="active", + assigned_role="BUILDER", + signal_count=0, + ) + session.add(other_project) + await session.flush() + session.add(UserProject(tenant_id=1, user_id=user.id, project_id=other_project.id)) + await session.flush() + charter = TruthNode( + tenant_id=1, + node_type="charter", + project_id=other_project.id, + created_by=user.id, + title="其它项目宪章", + content={"business_goal": "不能跨项目交接"}, + version=1, + status="aligned", + ) + session.add(charter) + await session.flush() + other_task = Task( + tenant_id=1, + truth_node_id=charter.id, + project_id=other_project.id, + assignee_id=user.id, + ai_recommended_assignees=None, + ai_breakdown_payload=None, + manual_overrides=None, + title="其它项目任务", + description="不能被当前项目 handoff", + required_skill_tag_ids=[], + status="confirmed", + priority="medium", + ) + session.add(other_task) + await session.commit() + app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER")) + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.post( + f"/pmflow-os/projects/{project.id}/execution-handoffs", + json={"task_id": str(other_task.id), "runtime_target": "codex"}, + ) + + assert response.status_code == 404 + assert response.json()["message"] == "task_not_found" + + @pytest.mark.asyncio async def test_project_permission_and_tenant_boundary(db_env) -> None: session = db_env diff --git a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md index e063985b..ff97d78d 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -91,7 +91,7 @@ ## 2. 下一步推进方向 -下一步不要扩大战线,不要新增“大模块”。正确方向是: +下一步不要扩大战线,不要新增新的战略产品表面;可以在已确认的 `Main Flow / Space / Account Settings` 这些既有表面内并行推进。正确方向是: **把 V1 主链真实闭环化。** @@ -345,7 +345,7 @@ ### 4.4 Codex 并行推进协议 -PMFlow 允许多个 Codex 线程并行推进,但并行单位必须是明确的 `Packet`,不能是“一个大模块”或“一个方向”。没有 `write_scope` 的任务不允许并行执行。 +PMFlow 允许多个 Codex 线程并行推进。产品上可以按“大模块 lane”长期并行,例如主流程、空间、个人中心,但实际执行单位仍必须是明确的 `Packet`。不能只用“做空间”“做个人中心”“做主流程”这类模糊大模块直接开工;每个模块 lane 都必须拆成带 `write_scope` 的 Packet。没有 `write_scope` 的任务不允许并行执行。 每个并行 `Packet` 开始前必须声明: @@ -370,6 +370,167 @@ PMFlow 允许多个 Codex 线程并行推进,但并行单位必须是明确的 - 并行线程不得在自己的 `write_scope` 外做“顺手修复”;发现阻塞问题时,只记录到 integration notes 或新建后续 Packet。 - 同一批并行开发必须指定一个 `integration owner`,负责最终合并共享文件、跑集成验证和更新进度台账。 +### 4.4.1 大模块并行治理 + +大模块并行的目标是让不同产品表面长期同时推进,但不让它们抢同一批共享文件、共享状态和共享合同。 + +大模块 lane 可以长期存在;Packet 必须短生命周期。lane owner 负责该产品表面的连续性,Packet owner 负责一次可验证增量。一个 lane 同一时间可以只有一个活跃 Packet,也可以拆成同 lane 内串行小 Packet;不同 lane 只有在 `write_scope` 不重叠时才允许并行。 + +大模块并行必须遵守以下规则: + +- 先由 `Shell / Integration owner` 冻结公共壳、路由、导航、Provider、共享 API client、共享文档归属。 +- `Main Flow / Space / Account Settings` 等模块线程只能写自己 lane 的 owned files。 +- 任一模块需要改共享文件,必须先把该修改拆给 `Shell / Integration owner`,不能在模块 Packet 里顺手改。 +- 模块之间只能通过稳定合同交互:URL、route params、只读 summary API、事件/待确认摘要、用户偏好 API;禁止跨模块直接 import 对方 store 内部实现。 +- Space 只能聚合和跳转,不直接写 Project Memory、ChangeSet commit、Execution Bridge 回写。 +- Account Settings 只能处理用户资料、偏好、通知和账户壳,不直接改项目主链、权限核心或 AI 路由。 +- Main Flow 可以继续推进 PMFlow OS 主链,但不得顺手重做 Space 首页或个人中心。 +- 每批大模块并行仍必须有一个 integration owner 统一执行 `Parallel Integration Gate`。 + +共享高冲突文件默认只归 `Shell / Integration owner`: + +- `frontend/src/router/index.tsx` +- `frontend/src/main.tsx` +- `frontend/src/lib/api.ts` 或其它共享 API base +- `frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx` +- `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` +- `backend/app/api/router.py` +- `backend/app/models/__init__.py` +- `backend/alembic/versions/*` +- `CLAUDE.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/architecture/API_CONTRACTS.md` + +### 4.4.2 当前大模块 lane 划分 + +#### Lane 0:Shell / Integration + +定位: + +- 冻结公共应用壳、路由、移动底部导航、Provider、共享文档和合并顺序。 +- 不承担具体产品表面的大量 UI 实现。 + +默认写范围: + +- `frontend/src/router/index.tsx` +- `frontend/src/main.tsx` +- `frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx` +- `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` +- `CLAUDE.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/architecture/API_CONTRACTS.md` + +#### Lane 1:Main Flow / PMFlow OS 主流程 + +定位: + +- 打穿 V1 主链:Project Thread -> Structured Extraction -> Change Review -> Project Memory -> Execution Bridge -> Release/Growth。 +- 这是产品核心闭环,优先级最高。 + +默认写范围: + +- `frontend/src/features/pmflow-os/` +- `frontend/src/api/pmflowOs.ts` +- `backend/app/api/pmflow_os_api/` +- `backend/app/services/pmflow_os_projection_service.py` +- `backend/app/services/pmflow_change_review_service.py` +- `backend/app/schemas/pmflow_os.py` +- `backend/tests/test_pmflow_*` + +默认禁止: + +- 不改 `frontend/src/features/personal-space/`。 +- 不改 Account Settings 域。 +- 不改共享 router / nav / docs,除非本 Packet 就是 integration owner。 + +#### Lane 2:Space / 个人空间入口 + +定位: + +- 建设 `/space` 作为所有项目、想法、草案、待确认和 AI 推进状态的上层入口。 +- Space 是聚合与导航层,不是 Project Memory 或 ChangeSet 的写入层。 + +默认写范围: + +- `frontend/src/features/personal-space/` +- `frontend/src/pages/PersonalSpacePage.tsx` +- `frontend/src/api/space.ts`(如需要) +- `backend/app/api/space_api/`(如需要,只读聚合优先) +- Space 相关测试 + +默认禁止: + +- 不改 `frontend/src/features/pmflow-os/store.tsx`。 +- 不改 PMFlow OS 主流程页面。 +- 不改 ChangeSet commit / Execution Bridge 写回服务。 +- 不直接写项目真相层;写动作必须跳转或调用已有主流程合同。 + +#### Lane 3:Account Settings / 个人中心与设置 + +定位: + +- 建设 `/me`、个人资料、偏好、通知、AI 使用偏好和账户操作。 +- Account Settings 是用户偏好层,不是项目协作层或权限核心重构。 + +默认写范围: + +- `frontend/src/features/account-settings/`(建议新建) +- `frontend/src/features/pmflow-os/pages/AccountPage.tsx` +- `frontend/src/features/pmflow-os/components/AccountControls.tsx` +- `frontend/src/api/accountSettings.ts`(如需要) +- `backend/app/api/account_settings_api/`(如需要) +- Account Settings 相关测试 + +默认禁止: + +- 不改 Project Memory / ChangeSet / Execution Bridge。 +- 不改 Space 聚合。 +- 不改 OAuth/JWT/auth core;如必须改,另开安全 Packet。 +- 不改共享 router / nav,除非由 integration owner 串行处理。 + +### 4.4.3 P0 公共壳冻结结果 + +截至 2026-04-26,P0 对当前共享壳的冻结结论如下。后续 `main-flow-phase-d-execution / space-v1-workbench / account-settings-v1` 必须按此边界开工。 + +#### Route 归属 + +| 路由 | 当前实现 | 归属 lane | 后续规则 | +| --- | --- | --- | --- | +| `/space` | 已存在,`PersonalSpaceStandalone` -> `frontend/src/pages/PersonalSpacePage.tsx` -> `frontend/src/features/personal-space/` | Space | Space lane 可改页面域;不得在本 lane 改 router,除非通过 Shell owner | +| `/project-launch` | 已存在,`ProjectLaunchStandalone` -> `frontend/src/pages/ProjectLaunchPage.tsx` -> `frontend/src/features/project-launch/` | Project Launch / Shell owned route | 非本批三线程主任务;后续只作为 Space 和 Main Flow 的入口/跳转消费 | +| `/pmflow-os` | 已存在,alias 到 `CommandCenterStandalone`,支持 `?projectId=` 真实项目上下文 | Main Flow | Main Flow lane 消费;route alias 归 Shell owner | +| `/command-center` | 已存在,standalone 与 AuthGuard children 双入口均存在 | Main Flow | 当前作为免登录产品预览壳和项目指挥台入口;后续是否收敛双入口由 Shell owner 单独处理 | +| `/change-review` | 已存在,移动底导“待确认”指向此路由 | Main Flow | Space 只能聚合摘要并跳转,不直接写 Change Review 状态 | +| `/execution-bridge` | 已存在于 AuthGuard children | Main Flow | Phase D 真实接入由 Main Flow lane 负责 | +| `/release-growth` | 已存在于 AuthGuard children | Main Flow | Phase E 再推进,不进入本批 Space / Account scope | +| `/me` | 已存在,standalone 与 AuthGuard children 双入口均存在,页面来自 `frontend/src/features/pmflow-os/pages/AccountPage.tsx` | Account Settings | Account lane 可改 AccountPage/AccountControls;route 归 Shell owner | +| `/settings` | 当前未实现 | Account Settings reserved | 不在 P0 临时新增;由 `account-settings-v1` 提出需要后,Shell owner 串行加 route | + +#### Provider 与共享状态 + +- `frontend/src/main.tsx` 当前全局挂载 `QueryClientProvider -> PMFlowOsProvider -> ToastProvider -> RouterProvider`。 +- `PMFlowOsProvider` 当前是全局 provider,但业务所有权归 `Main Flow / PMFlow OS`。 +- Space 和 Account Settings 可读取稳定公开 hook/API,但禁止直接改 `frontend/src/features/pmflow-os/store.tsx` 或依赖其内部 mutation 细节。 +- 如果 Space 需要“待确认 / AI 正在推进 / 最近项目”摘要,优先新增 `frontend/src/api/space.ts` 与只读后端聚合接口,不能从 PMFlow OS store 内部反向拼。 +- 如果 Account Settings 需要用户偏好,优先新增 `frontend/src/api/accountSettings.ts` 与独立后端接口,不能把个人偏好塞进 PMFlow OS truth store。 + +#### 导航与移动底部栏 + +- `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航实现。 +- 当前五项固定为:`空间 -> /space`、`指挥台 -> /command-center`、`待确认 -> /change-review`、`动态 -> /command-center`、`我的 -> /me`。 +- Space / Main Flow / Account 三个模块线程不得各自改底部导航。任何导航项新增、重命名、跳转变更,都必须交给 Shell owner。 +- 当前 `动态` 暂时回到 `/command-center`,属于保留占位;后续如实现 `/activity` 或动态页,必须单独开 Shell / Integration Packet。 + +#### 下一批开工条件 + +- `main-flow-phase-d-execution` 可以直接开工,前提是不改 router、MobileBottomNav、PMFlowOsFrame 的导航归属。 +- `space-v1-workbench` 可以直接开工,前提是只改 `personal-space` owned files 和可选 `space.ts / space_api`,不碰 PMFlow OS store。 +- `account-settings-v1` 可以直接开工,前提是不新增 `/settings` route;如确需 `/settings`,先把 route 需求写入 integration notes,再由 Shell owner 添加。 + ### 4.5 V1 Productized Loop 默认并行切片 V1 主链并行开发默认按 lane 切分: @@ -554,7 +715,7 @@ commit gate 判定: 当前最建议下一步执行: -**Phase D:Execution Bridge 真实数据接入。** +**Phase D.2:Execution Bridge 执行交接合同。** 原因: @@ -562,56 +723,259 @@ commit gate 判定: - Phase C1 已把项目内消息、结构化候选、审查动作和证据链落到真实表。 - C2-contract 已锁定 commit API、原子提交规则、响应合同和失败语义。 - C2-backend 已按合同实现 atomic commit route/service/schema/tests,C2-frontend 已把真实模式写入接到 commit API。 -- 当前缺口是执行桥页面仍偏 demo 编排,下一步应把 `ExecutionBridgePage` 切到 `execution-summary` 与 dev-sync 真实数据。 +- Phase D 已把 `ExecutionBridgePage` 切到 `execution-summary` 与 dev-sync 真实数据。 +- 当前缺口是执行桥只读展示已经成立,但还不能把一个已确认任务推进到可追踪的真实执行交接;下一步应实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,生成/复用 PromptPackage、绑定 task/repo/runtime,并创建 `handoff_created` progress event 起点。 -### 7.1 Phase A 建议拆成三个小任务 +### 7.1 下一批大模块并行建议 -#### A1:对象合同映射 +P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行;最终仍必须由 `Batch P2` integration owner 收口。 + +#### Batch P0:Shell / Route / Shared Contract Freeze + +状态:`done` + +冻结结论: + +- 当前已存在 `/space`、`/project-launch`、`/pmflow-os`、`/command-center`、`/change-review`、`/execution-bridge`、`/release-growth`、`/me`。 +- `/settings` 当前未实现,标记为 Account Settings reserved route;不得由业务模块线程顺手新增。 +- 移动端底部导航已固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`,实现归 `Shell / Integration owner`。 +- `frontend/src/main.tsx` 全局 provider 顺序已确认:`QueryClientProvider -> PMFlowOsProvider -> ToastProvider -> RouterProvider`;后续 provider 变更必须由 Shell owner 串行处理。 + +#### Batch P1:三模块并行 + +在 P0 完成后,可以同时开三个长期 lane 的首个 Packet。 + +##### Main Flow Packet:`main-flow-phase-d2-execution-handoff-contract` 目标: -把 `frontend/src/features/pmflow-os/types.ts` 里的对象逐一映射到现有后端对象或新增对象。 +- 继续执行 Phase D.2,把 Execution Bridge 从 execution dashboard 推进为 execution command bridge。 +- 新增 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,允许对已确认任务发起真实执行交接。 +- 后端生成/复用 `PromptPackage`,记录 `runtime_target / repo_binding_id / task_id`,并创建 `ProgressEvent(event_type='handoff_created')`。 +- 前端真实模式下“发起执行交接”调用真实 API,成功后 refetch `execution-summary / truth-state`;无 task/repo/prompt 时必须显示明确 warning 和下一步。 -输出: +默认写范围: -- 一张合同映射表。 -- 明确复用 `TruthNode / Task / semantic facts / ai_project_control` 的边界。 -- 明确是否需要新增 `pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`。 +- `frontend/src/features/pmflow-os/` +- `frontend/src/api/pmflowOs.ts` +- `backend/app/api/pmflow_os_api/` +- `backend/app/services/pmflow_os_projection_service.py` +- `backend/app/schemas/pmflow_os.py` +- 相关 tests -#### A2:API 合同设计 +##### Space Packet:`space-v1-workbench` 目标: -定义 PMFlow OS 读写 API: +- 把 `/space` 推进成个人工作面聚合页。 +- 展示继续推进、待我确认、AI 正在推进、暂存想法、草案箱、最近项目。 +- 优先只读聚合;点击项目进入 `/pmflow-os?projectId=...`,不直接写项目真相层。 -- 获取项目线程和当前 Project Memory。 -- 创建结构化提取请求。 -- 获取 ChangeSet。 -- 接受/修改/暂缓 ChangeItem。 -- 将确认结果写入 Work Graph / Project Memory。 -- 获取 Execution Bridge summary。 +默认写范围: -输出: +- `frontend/src/features/personal-space/` +- `frontend/src/pages/PersonalSpacePage.tsx` +- `frontend/src/api/space.ts`(如需要) +- `backend/app/api/space_api/`(如需要,只读聚合优先) +- Space 相关测试 -- Pydantic schema。 -- API 路由设计。 -- API response envelope。 - -#### A3:最小真实读路径 +##### Account Settings Packet:`account-settings-v1` 目标: -先让 `CommandCenter / ProjectMemory / WorkGraph` 从真实后端读取一个项目的摘要和正式对象。 +- 建设 `/me` 和个人设置首版。 +- 覆盖个人资料、AI 偏好、通知偏好、账户操作和移动端 bottom sheet。 +- 不改 OAuth/JWT/auth core,不改项目主链。 -输出: +默认写范围: -- 前端 API 封装。 -- React Query 接入。 -- 本地 mock 降为 fallback/demo,不再是正式路径。 -- 最小测试。 +- `frontend/src/features/account-settings/`(建议新建) +- `frontend/src/features/pmflow-os/pages/AccountPage.tsx` +- `frontend/src/features/pmflow-os/components/AccountControls.tsx` +- `frontend/src/api/accountSettings.ts`(如需要) +- `backend/app/api/account_settings_api/`(如需要) +- Account Settings 相关测试 + +#### Batch P2:Integration Owner 收口 + +目标: + +- 检查三个 Packet 的 `git diff --name-only` 是否越界。 +- 统一处理 router、bottom nav、shared docs、API index 的冲突。 +- 跑后端/前端最小集成验证。 +- 更新 `## 8. 进度台账`、`CODE_REFERENCE_INDEX.md`、`ARCHITECTURE_INVENTORY.md` 与必要的 `CLAUDE.md`。 ## 8. 进度台账 +### 2026-04-26 / Space V1 Workbench Hardening / implemented + +完成内容: + +- 将 `/space` 收敛为只读个人工作面聚合入口,继续覆盖 `继续推进 / 待我确认 / AI 正在推进 / 暂存想法 / 草案箱 / 最近项目`,不写 Project Memory、ChangeSet 状态或 Execution Bridge event。 +- `frontend/src/api/space.ts` 作为 Space read model wrapper,仅消费项目摘要、PMFlow OS `truth-state` 与 `execution-summary`;项目主动作仍进入 `/pmflow-os?projectId=`。 +- 待确认项只读取 ChangeSet / approvals 摘要,点击进入稳定主流程 `/change-review?projectId=&changeSetId=`,不直接执行 accept/edit/defer/commit。 +- 暂存想法合同已显式建模为 `savedIdeas`;当前后端只读聚合未接入时返回空数组并展示 warning,禁止用 mock/demo 数据冒充真实想法。 +- 移动端继续使用 Space 独立单手路径,补齐最近项目区块,不把桌面多栏布局压缩到手机。 +- 补充 `PersonalSpacePage.test.tsx` focused 回归,锁定六个 Space 区块与“暂存想法未接入不冒充真实数据”的空态。 + +涉及文件: + +- `frontend/src/api/space.ts` +- `frontend/src/features/personal-space/PersonalSpacePage.tsx` +- `frontend/src/features/personal-space/PersonalSpacePage.test.tsx` +- `frontend/src/features/personal-space/components/SpaceChrome.tsx` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `cd frontend && npm run test -- src/features/personal-space/PersonalSpacePage.test.tsx` 通过。 +- `cd frontend && npm run typecheck` 未通过,阻塞来自 Main Flow 越界文件 `frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx:21` 的既有 `import type` 语法错误,本 Space 包不跨线修。 +- `cd frontend && npm run build` 未通过,同样阻塞于 `frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx:21`,未进入 Vite build 阶段。 + +剩余缺口: + +- 暂存想法真实只读聚合后端合同尚未实现;当前 Space 明确暴露为空态和 warning,不使用演示数据兜底。 + +### 2026-04-26 / Account Settings V1 / implemented + +完成内容: + +- 新增 `frontend/src/api/accountSettings.ts`,定义个人资料、默认语言、AI 回答深度、证据展示偏好和通知偏好前端合同;首版用浏览器本地持久化承接设置保存,不写 PMFlow OS truth store。 +- 新增 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`,承接 `/me` 桌面设置页与移动端分组列表 + bottom sheet。 +- 将 `/me` 的 `AccountPage.tsx` 改为薄入口,继续复用 `AccountControls.tsx` 的退出登录 bottom sheet,不改 router、MobileBottomNav、PMFlow OS store 或 OAuth/JWT/auth core。 +- 通知偏好已覆盖 `待确认 / 执行回写 / 发布门禁 / 失败提醒`,每类支持总开关、桌面提醒和邮件摘要。 +- 保存状态已收口为 `仅本机暂存 / 已保存到当前账号 / 保存失败` 三态;当前 V1 只做本机暂存,不假装云端账号保存。 +- 补充 `frontend/src/features/account-settings/accountSettings.test.ts`,锁定默认设置和持久化行为。 + +涉及文件: + +- `frontend/src/api/accountSettings.ts` +- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` +- `frontend/src/features/account-settings/accountSettings.test.ts` +- `frontend/src/features/pmflow-os/pages/AccountPage.tsx` +- `CLAUDE.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `npm run test -- src/features/account-settings/accountSettings.test.ts` 通过:`3 passed`。 +- `npm run typecheck` 通过。 +- `npm run build` 通过;Vite 仍输出既有 manual chunk circular warning,未阻断构建。 +- Playwright 本地浏览器检查通过:`http://localhost:5186/me` 桌面与 390px 移动视口均可渲染,移动端通知偏好 bottom sheet 可打开。 + +未完成或风险: + +- 当前 Account Settings V1 是前端合同 + 本地持久化,后续如要跨设备同步,需要独立新增 `backend/app/api/account_settings_api/` 和持久化表,不能塞入 auth core 或 PMFlow OS truth store。 + +### 2026-04-26 / P0 Shell Module Boundaries / done + +完成内容: + +- 核对当前公共壳真实落点:`frontend/src/router/index.tsx`、`frontend/src/main.tsx`、`MobileBottomNav.tsx`、`PMFlowOsFrame.tsx`。 +- 冻结大模块 route 归属:`/space` 归 Space,`/project-launch` 作为启动层入口,`/pmflow-os / /command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow,`/me` 归 Account Settings。 +- 明确 `/settings` 当前未实现,仅作为 Account Settings reserved route;后续如需新增,必须由 Shell / Integration owner 串行处理。 +- 冻结全局 provider 归属:`QueryClientProvider -> PMFlowOsProvider -> ToastProvider -> RouterProvider`,Space / Account 不得直接改 PMFlow OS store 内部 mutation。 +- 冻结移动端底部导航归属:唯一实现为 `MobileBottomNav.tsx`,固定 `空间 / 指挥台 / 待确认 / 动态 / 我的`;其中 `动态` 仍是 `/command-center` 占位,后续要做 `/activity` 必须单独开 Shell Packet。 +- 同步修正 `ARCHITECTURE_INVENTORY.md` 中 C1/C2 commit 的旧描述,明确 C2 backend/frontend commit 已完成。 + +涉及文件: + +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `CLAUDE.md` + +验证: + +- `git diff --name-only` 已确认本轮只改文档与约束文件,未改前端共享壳代码。 +- 本轮未修改前端代码,因此未运行 `npm run typecheck`。 + +未完成或风险: + +- `/settings` 尚未实现;`account-settings-v1` 如需要该入口,必须先把 route 需求交给 Shell / Integration owner。 +- `动态` 底部导航仍指向 `/command-center`,后续如做动态页需独立 Packet。 + +下一步建议: + +- 并行启动 `main-flow-phase-d-execution`、`space-v1-workbench`、`account-settings-v1`;三个 Packet 均不得改 router/nav/provider/shared docs,最终由 integration owner 收口。 + +### 2026-04-26 / Phase D.2 - Execution Handoff Contract / done + +完成内容: + +- 新增 `POST /pmflow-os/projects/{project_id}/execution-handoffs` 后端入口,复用 PMFlow OS 项目成员/OWNER/REVIEWER 权限边界。 +- `PMFlowOSProjectionService.create_execution_handoff` 校验 task/project/tenant 边界,校验可选 active repo binding;无 repo 时允许创建 handoff draft 并保留 repo warning。 +- 对任务复用 active `PromptPackage`;没有 active package 时,从真实 `Project / Task / Project Memory / RepoBinding` 生成 deterministic `PromptPackage(package_kind='execution_handoff')`,不使用 demo 文案。 +- 创建 `ProgressEvent(event_type='handoff_created', source='pmflow_os')` 作为执行起点,payload 记录 `runtime_target / prompt_package_id / repo_binding_id / boundary`。 +- 前端 `ExecutionBridgePage` 在真实模式展示“发起执行交接”按钮,调用 `createPMFlowExecutionHandoff` 后 refetch `truth-state / execution-summary`;demo 模式仍只走本地推进。 +- 无 active task、无 repo、无 prompt package 时继续显示明确 empty/warning 和下一步,不回退假数据。 + +涉及文件: + +- `backend/app/api/pmflow_os_api/__init__.py` +- `backend/app/services/pmflow_os_projection_service.py` +- `backend/app/schemas/pmflow_os.py` +- `backend/tests/test_pmflow_os_projection_api.py` +- `frontend/src/api/pmflowOs.ts` +- `frontend/src/features/pmflow-os/store.tsx` +- `frontend/src/features/pmflow-os/store.api.test.tsx` +- `frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx` +- `frontend/src/features/pmflow-os/components/ExecutionBridgeCards.tsx` +- `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m py_compile backend/app/api/pmflow_os_api/__init__.py backend/app/services/pmflow_os_projection_service.py backend/app/schemas/pmflow_os.py` 通过。 +- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m pytest backend/tests/test_pmflow_os_projection_api.py -q` 通过:`9 passed`。 +- `cd frontend && npm run test -- src/features/pmflow-os` 通过。 +- `cd frontend && npm run typecheck` 通过。 +- `cd frontend && npm run build` 通过;Vite 仍输出既有 manual chunk circular warning,未阻断构建。 + +未完成或风险: + +- D.2 只创建 handoff 合同、PromptPackage 与 progress event 起点,不直接执行本机命令;真实命令执行仍必须通过 Local Bridge、Codex、GitHub agent 或兼容 MCP host。 +- 当前未新增 migration,沿用既有 `PromptPackage / RepoBinding / ProgressEvent`。 + +下一步建议: + +- 进入 D.3:补 Dev Sync 写回闭环,把 Codex/Local Bridge/GitHub agent 的执行结果回写为 branch/commit/PR/test progress event,并把任务状态/证据链投影回 `truth-state`。 + +### 2026-04-26 / Big Module Parallel Governance / done + +完成内容: + +- 将大模块并行治理写入产品推进标准:允许 `Main Flow / Space / Account Settings` 作为长期模块 lane 并行,但实际执行仍必须拆成带 `write_scope` 的短生命周期 Packet。 +- 明确 `Shell / Integration owner` 负责共享路由、导航、Provider、共享 API base、共享文档和合并顺序。 +- 定义当前四条 lane:`Shell / Integration`、`Main Flow / PMFlow OS 主流程`、`Space / 个人空间入口`、`Account Settings / 个人中心与设置`。 +- 明确每条 lane 的默认写范围、默认禁止范围和跨模块交互边界。 +- 更新下一批推荐任务:先执行 `P0-shell-module-boundaries`,再并行推进 `main-flow-phase-d-execution / space-v1-workbench / account-settings-v1`,最后由 integration owner 收口。 + +涉及文件: + +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` +- `docs/architecture/TASK_BRIEF_TEMPLATE.md` +- `AGENTS.md` +- `CLAUDE.md` + +验证: + +- 文档静态检查完成;本轮只更新治理文档和任务模板,未改代码、API、Schema 或 migration,未运行编译/单测。 + +未完成或风险: + +- 该记录是大模块治理规则落盘;具体公共壳真实路由和 provider 核对已由后续 `P0 Shell Module Boundaries` 完成。 + +下一步建议: + +- 后续按 P0 冻结结果同时分派 `main-flow-phase-d-execution`、`space-v1-workbench`、`account-settings-v1` 三个大模块 Packet。 + ### 2026-04-26 / Phase C2 Frontend - ChangeSet Commit Wiring / done 完成内容: diff --git a/docs/architecture/ARCHITECTURE_INVENTORY.md b/docs/architecture/ARCHITECTURE_INVENTORY.md index 71e4c2bf..84dd208d 100644 --- a/docs/architecture/ARCHITECTURE_INVENTORY.md +++ b/docs/architecture/ARCHITECTURE_INVENTORY.md @@ -23,7 +23,7 @@ - `docs/INBOX_INLINE_OPS_3_PHASE_CHECKLIST.md`:消息台内联操作执行清单 - `docs/architecture/MASTER_CONTRACT.md`:跨栈主契约 - `docs/architecture/CODE_REFERENCE_INDEX.md`:新增代码参考入口与高频代码索引 -- `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`:PMFlow OS 产品合同真源;锁定前端 `ProjectTruthState / ChangeSet / Evidence / Approval / ExecutionContext` 与后端 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的复用边界;Phase C1 已实现 review 表和审查 API,commit 仍未实现 +- `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`:PMFlow OS 产品合同真源;锁定前端 `ProjectTruthState / ChangeSet / Evidence / Approval / ExecutionContext` 与后端 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的复用边界;Phase C1 已实现 review 表和审查 API,Phase C2 backend/frontend 已实现 atomic commit - `docs/architecture/RUNTIME_GUARDRAILS.md`:运行边界、守门规则与已知运行假设 - `docs/architecture/API_CONTRACTS.md`:接口/Schema 契约 - `docs/architecture/DATAFLOW.md`:主链与数据流说明 @@ -172,6 +172,7 @@ - `frontend/src/components/login/` - `frontend/src/features/pmflow-os/pages/` - `frontend/src/features/pmflow-os/components/` +- `frontend/src/features/account-settings/` - `frontend/src/features/personal-space/components/` - `frontend/src/pages/skill-tasks/` - `frontend/src/pages/skills/` @@ -192,12 +193,13 @@ - `frontend/src/components/ai-steward/QAResponseMetaCard.test.tsx` - AI Steward 问答浮窗现分三层显性化:`FloatingQAChat.tsx` 负责即时回答/历史回放归一化,`QAConversationStatusBar.tsx` 展示对象/来源/仓库/执行通道/plan handoff,`QAResponseMetaCard.tsx` 展示 `fit_brief / external_recommendation / recommended_next_step`,对应前端回归由 `FloatingQAChat.test.tsx` 与 `QAResponseMetaCard.test.tsx` 锁定。 - 登录页与营销页仍共享 `frontend/src/components/marketing/` 的部分视觉基线;公开登录页现改由 `frontend/src/components/login/` 目录承载,其中 `LoginExperience.tsx` 负责登录壳层、`LoginWorkflowVisual.tsx` 负责左侧场景、`loginScene.ts` / `loginMath.ts` 拆出展示配置与路径几何、`login.css` 负责登录页专属样式边界,`frontend/src/pages/login/LoginPage.tsx` 继续负责真实鉴权、错误态和 redirect 提示;未登录访问 `/` 会经 `frontend/src/router/index.tsx` 直接重定向到 `/login`,无显式 `redirect` 的成功登录现在默认进入 `/command-center`。 -- 前端主包治理已切到“同步首屏最小集 + route/page-section lazy”模式:`frontend/src/router/index.tsx` 保留 `LoginPage / Layout` 这组同步入口,其余后台重页面通过 `React.lazy + Suspense` 懒加载;统一 fallback 在 `frontend/src/components/layout/LazyBoundary.tsx`。`frontend/src/features/personal-space/` 作为空间层入口,通过 `/space` 提供可直接打开的本地预览入口,承载继续推进、待我确认、AI 正在推进、暂存想法、草案箱与新建项目入口。`frontend/src/features/project-launch/` 作为首次进入页 / 新项目启动台产品域,通过 `/project-launch` 提供可直接打开的入口;Phase B 后默认调用真实 `pmflow-launch` draft/confirm API,创建成功态主动作跳转后端返回的 `/pmflow-os?projectId=`,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用本地 mock 状态机。`frontend/src/features/pmflow-os/` 作为新产品域,通过 `PMFlowOsProvider` 在应用根部挂载共享 truth store,并提供 `CommandCenter / ChangeReview / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主入口;当前 PMFlow OS 页面已切到专属轻壳,桌面端默认隐藏旧 TopBar,并由 `frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx` 统一承接“左侧稳定导航 + 顶部上下文条 + 中央视图 + 右侧 AI 副驾”的全局骨架,同时承接项目内 `个人空间 / 当前项目名` 面包屑和项目切换器;账号退出登录由 `frontend/src/features/pmflow-os/components/AccountControls.tsx` 统一承接桌面头像下拉、退出确认与移动端 bottom sheet,`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 提供 `/me` 移动“我的”页;正式应用壳移动端底部导航统一由 `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` 承接;`/command-center` 额外提供免登录产品预览壳,`/pmflow-os` 是真实项目上下文 alias。主视觉按 `#2463EB + 白色` 的 Linear-like 高密度原型展开。`UsersPage` 把创建/删除/重置/导入/批量分配对话框拆为按需 chunk,`UserSkillsPage` 与 `WorkloadDetailPage` 的图表也分别拆到了 `frontend/src/pages/user-skills/components/UserSkillsRadarPanel.tsx` 与 `frontend/src/pages/workload-detail/components/WorkloadTrendChart.tsx`。 -- `frontend/src/features/personal-space/mockData.ts` 与 `PersonalSpacePage.tsx` 是空间首页本地真相和视图层;移动端为空间首页独立 App 布局,不直接压缩桌面卡片;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间,不做团队空间强切换、企业组织管理或传统项目列表后台。 +- 前端主包治理已切到“同步首屏最小集 + route/page-section lazy”模式:`frontend/src/router/index.tsx` 保留 `LoginPage / Layout` 这组同步入口,其余后台重页面通过 `React.lazy + Suspense` 懒加载;统一 fallback 在 `frontend/src/components/layout/LazyBoundary.tsx`。P0 公共壳冻结后,大模块 route 归属为:`/space` 归 Space lane,`/project-launch` 是启动层入口,`/pmflow-os` 与 `/command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route、尚未实现,新增必须由 Shell / Integration owner 串行处理。`frontend/src/features/personal-space/` 作为空间层入口,通过 `/space` 提供可直接打开的本地预览入口,承载继续推进、待我确认、AI 正在推进、暂存想法、草案箱与新建项目入口。`frontend/src/features/project-launch/` 作为首次进入页 / 新项目启动台产品域,通过 `/project-launch` 提供可直接打开的入口;Phase B 后默认调用真实 `pmflow-launch` draft/confirm API,创建成功态主动作跳转后端返回的 `/pmflow-os?projectId=`,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用本地 mock 状态机。`frontend/src/features/account-settings/` 作为 Account Settings 产品域,通过 `/me` 提供个人资料、默认语言、AI 回答深度、证据展示偏好、待确认/执行回写/发布门禁/失败提醒通知偏好和账户操作入口;该域首版通过 `frontend/src/api/accountSettings.ts` 建立前端合同与本地持久化,不修改 OAuth/JWT/auth core。`frontend/src/features/pmflow-os/` 作为新产品域,通过 `PMFlowOsProvider` 在应用根部挂载共享 truth store,并提供 `CommandCenter / ChangeReview / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主入口;当前 PMFlow OS 页面已切到专属轻壳,桌面端默认隐藏旧 TopBar,并由 `frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx` 统一承接“左侧稳定导航 + 顶部上下文条 + 中央视图 + 右侧 AI 副驾”的全局骨架,同时承接项目内 `个人空间 / 当前项目名` 面包屑和项目切换器;账号退出登录由 `frontend/src/features/pmflow-os/components/AccountControls.tsx` 统一承接桌面头像下拉、退出确认与移动端 bottom sheet,`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 提供 `/me` 入口并委托 Account Settings 域渲染设置页;正式应用壳移动端底部导航统一由 `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` 承接,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`,其中 `动态` 当前仍是 `/command-center` 占位。`/command-center` 额外提供免登录产品预览壳,`/pmflow-os` 是真实项目上下文 alias。主视觉按 `#2463EB + 白色` 的 Linear-like 高密度原型展开。`UsersPage` 把创建/删除/重置/导入/批量分配对话框拆为按需 chunk,`UserSkillsPage` 与 `WorkloadDetailPage` 的图表也分别拆到了 `frontend/src/pages/user-skills/components/UserSkillsRadarPanel.tsx` 与 `frontend/src/pages/workload-detail/components/WorkloadTrendChart.tsx`。 +- `frontend/src/features/personal-space/PersonalSpacePage.tsx` 是空间首页视图层,`frontend/src/api/space.ts` 是空间工作面只读聚合 API:从 `projects` summary、PMFlow OS `truth-state` 与 `execution-summary` 生成继续推进、待确认、AI 正在推进、暂存想法、草案箱、最近项目摘要;项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入或 execution event 写入。 - `frontend/src/features/project-launch/types.ts`、`mockData.ts`、`useProjectLaunchState.ts` 是新项目启动台状态层;默认真实模式通过 `frontend/src/api/pmflowLaunch.ts` 生成草案并确认创建真实项目,API 失败进入明确失败态,不静默 fallback 成 mock 成功;`mockData.ts` 仅在显式 `?demo=1` 或 `?dataSource=demo` 下作为演示状态机使用。`components/LaunchShell.tsx` 负责桌面/移动分流,桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,`LaunchCanvases.tsx` 和 `MobileLaunchLayout.tsx` 承接高保真图中的意图输入、AI 工作、人类确认、草案修改、生成失败、创建成功过渡与移动端 bottom sheet 交互。 -- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer` 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。 -- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、PR/测试与回写状态页;A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,但仍不调用真实 AI 提取、不实现 commit、不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只留在 review 投影中,不进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。 -- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,C1 审查状态机服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 只实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`,不实现 commit,不写 `truth_nodes / tasks / semantic_facts`。 +- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` 是 `/me` 首版设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好。`frontend/src/api/accountSettings.ts` 是前端合同与本地持久化层;V1 UI 明确区分 `仅本机暂存 / 已保存到当前账号 / 保存失败`,当前实现只使用本机暂存,不假装云端账号保存;后续如接后端,应新增独立 `account_settings_api`,不能写入 PMFlow OS truth store 或 auth core。 +- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。 +- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、PR/测试与回写状态页;A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。 +- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点,不直接执行本机命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。 - PMFlow Launch 后端 Phase B 创建链位于 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与 `backend/app/models/pmflow_launch.py`;`POST /pmflow-launch/drafts` 只写 `pmflow_launch_drafts`,`POST /pmflow-launch/drafts/{draft_id}/confirm` 在同一事务内写 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`,并把 `launch_draft_id / thread_id / thread_message_id` 写入正式对象 metadata。 - `frontend/src/features/pmflow-os/components/ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx` 是高保真原型新增的行为组件层:分别承载变更 diff 审查块、工作台对象卡组、执行桥上下文/日志卡片、全局上下文条与 AI 副驾、账号菜单/退出登录确认交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组提示、提醒、确认与移动端 bottom sheet 基础组件,避免再把这些业务片段内联塞回 `pages/`。 - `frontend/vite.config.ts` 现补了固定 vendor chunk 分组(`framework / state-query / ui-radix / icons / charts / markdown`)与业务 chunk 分组(`ai-steward / sstc / governance`),目标是让登录页与基础导航不再背 AI Steward、SSTC、治理页和图表代码。 diff --git a/docs/architecture/CODE_REFERENCE_INDEX.md b/docs/architecture/CODE_REFERENCE_INDEX.md index b1d3f35d..df86bb67 100644 --- a/docs/architecture/CODE_REFERENCE_INDEX.md +++ b/docs/architecture/CODE_REFERENCE_INDEX.md @@ -89,27 +89,30 @@ - SSTC 负责人推荐接口:参考 `POST /projects/{project_id}/tasks/{task_id}/recommend-assignee` - SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}` - SSTC 发布创建接口:参考 `POST /projects/{project_id}/releases` -- PMFlow OS 真实读路径、C1 Change Review API 与 C2 backend commit:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate +- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit 与 D.2 execution handoff:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=atomic`,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点,不直接执行本机命令 - Project Launch 真实创建链:参考 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与模型 `backend/app/models/pmflow_launch.py`;接口为 `POST /pmflow-launch/drafts` 与 `POST /pmflow-launch/drafts/{draft_id}/confirm`,draft 只写 `pmflow_launch_drafts`,confirm 在同一事务内创建 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs` - PMFlow Change Review 模型与 migration:参考 `backend/app/models/pmflow_change_review.py` 与 `backend/alembic/versions/20260426_add_pmflow_change_review_tables.py`;只包含 `pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`,本阶段不得新增或依赖 `/commit` ## 3. 前端参考 - PMFlow UI 契约真源:先读 `docs/PMFLOW_UI_CONTRACT.md`,再修改 `project-launch`、`personal-space`、`pmflow-os` 相关页面;该文档锁定 Logo、色彩、移动端底部导航、状态标签、AI 操作/用户动作/待确认视觉语法和核心功能 canonical 契约 +- PMFlow 大模块并行公共壳:参考 `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` 的 `4.4.1-4.4.3` 与 `frontend/src/router/index.tsx`、`frontend/src/main.tsx`、`frontend/src/features/pmflow-os/components/MobileBottomNav.tsx`、`frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx`;`/space` 归 Space lane,`/pmflow-os / /command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route,新增或改动必须由 Shell / Integration owner 串行处理 - PMFlow OS 产品合同真源:先读 `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`,再实现 `pmflow-os` 真实后端接入;该文档锁定 `ProjectTruthState / Thread / ChangeSet / ChangeItem / Evidence / Approval / Project Memory / ExecutionContext` 与 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的复用边界和后续 API 草案 -- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` +- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory 或 execution event;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` - AI Steward 页面入口:参考 `frontend/src/pages/ai-steward/` - AI Steward 问答浮窗与上下文显性化组件:参考 `frontend/src/components/ai-steward/FloatingQAChat.tsx`、`frontend/src/components/ai-steward/QAConversationStatusBar.tsx`、`frontend/src/components/ai-steward/QAResponseMetaCard.tsx` - AI Steward QA contract 前端类型(含 `analysis_packet`):参考 `frontend/src/api/aiSteward.ts` - AI Steward handoff 元卡片回归:参考 `frontend/src/components/ai-steward/FloatingQAChat.test.tsx`、`frontend/src/components/ai-steward/QAResponseMetaCard.test.tsx` - AI Steward / QA session 前端 contract:参考 `frontend/src/api/aiSteward.ts` - PMFlow OS 前端 MVP 产品域:参考 `frontend/src/features/pmflow-os/` -- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、AI 正在推进、暂存想法、草案箱与新建项目入口;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件 +- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`;Space 只读聚合项目摘要、PMFlow OS `truth-state / execution-summary`,不直接写 Project Memory、ChangeSet 状态或 execution event;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件 - 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 draft/confirm API,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用 `mockData.ts` 本地演示状态机;本地预览路由入口为 `/project-launch` -- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=` 走 React Query 读取真实 `truth-state`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer` 调后端并 refetch,不本地 promote 入 Project Memory;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success` +- Account Settings / `/me` 前端产品域:参考 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`、`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 与 `frontend/src/api/accountSettings.ts`;当前首版覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,移动端使用分组列表与 bottom sheet,账户退出仍复用 `AccountControls.tsx`,不改 OAuth/JWT/auth core;V1 暂为本机持久化,UI 必须明确显示 `仅本机暂存 / 保存失败`,不得假装云端账号保存 +- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff 均调后端并 refetch,不本地 promote 入 Project Memory 或伪造 execution event;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success` - PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx` - PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx`、`ProjectStageRail.tsx`、`ThreadComposer.tsx`、`ExtractionRail.tsx`、`ChangeGroupPanel.tsx`、`ObjectGraphSection.tsx`、`ContextPacketPanel.tsx`、`ExecutionTimeline.tsx`、`ReleaseGatePanel.tsx`、`TeamFeedPanel.tsx`、`ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx`、`MobileBottomNav.tsx`;其中 `ProjectStructuredLoop.tsx` 承接 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel`,`PMFlowOsFrame.tsx` 承接项目内面包屑和项目切换器,`AccountControls.tsx` 承接桌面头像菜单、退出登录确认与移动端 bottom sheet,`FeedbackPrimitives.tsx` 承接 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet`,`MobileBottomNav.tsx` 承接正式应用壳移动端固定底部导航 - PMFlow OS 原型状态回归测试:参考 `frontend/src/features/pmflow-os/store.test.ts`;真实 API 动作回归参考 `frontend/src/features/pmflow-os/store.api.test.tsx` - Project Launch 真实/演示分支回归测试:参考 `frontend/src/features/project-launch/useProjectLaunchState.test.ts` +- Account Settings 前端合同回归测试:参考 `frontend/src/features/account-settings/accountSettings.test.ts` - 网页问答显式锚点与 AI Steward URL 构造:参考 `frontend/src/components/ai-steward/qaPageAnchor.ts` - Inbox 页面入口:参考 `frontend/src/pages/inbox/` - Inbox `plan_mode` 展开卡片与映射:参考 `frontend/src/components/inbox/InboxListItem.tsx`、`frontend/src/lib/inbox.ts`、`frontend/src/api/aiPmo.ts` diff --git a/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md b/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md index b0affc7c..c6d14439 100644 --- a/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md +++ b/docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md @@ -61,7 +61,7 @@ pmflow_evidence_links | `TestRun` | `model_eval_runs`、`release_validation_runs`、`progress_events(event_type=test*)` 投影 | 否 | `truth-state`、`execution-summary` | dev-sync progress event 或 release validation | `pending/running -> passed/failed/blocked` | 测试日志和 PR/commit 可作为 evidence | 当前没有通用 test_runs 表,V1 先按投影聚合 | | `Release` | `truth_nodes.node_type='release'` + `release_validation_runs` | 否 | `truth-state`;SSTC release APIs | SSTC release 创建;`run_release_validation` | `check_pending -> ready -> shipping -> live/watch` | release gate、rollback_plan、validation_summary | 发布对象和发布验证结果需要在 projection 中分清 | | `MetricSignal` | `feedback_ingests.metrics`、`revenue_events`、`roi_snapshots`、增长埋点结果投影 | 否 | `truth-state`;`/roi-snapshots` | `feedback-ingest`、`revenue-events` | `draft/proposed -> confirmed` | 指标来源和采样窗口必须展示 | 只做轻量反馈,不引入完整财务系统 | -| `ExecutionContext` | `prompt_packages`、`repo_bindings`、`task_code_links`、`progress_events`、`dev_sessions` | 否 | `GET /pmflow-os/projects/{project_id}/execution-summary` 聚合 `GET /projects/{project_id}/dev-sync/summary` | `createPromptPackage`、`bindGithubRepo`、`createProgressEvent` | `queued -> pushed -> coding -> pr_open -> testing -> synced/failed` | progress events、deliveries、repo/PR/commit 是执行证据 | 不得重造 dev-sync;PMFlow OS 只做项目内执行投影 | +| `ExecutionContext` | `prompt_packages`、`repo_bindings`、`task_code_links`、`progress_events`、`dev_sessions` | 否 | `GET /pmflow-os/projects/{project_id}/execution-summary` 聚合 `GET /projects/{project_id}/dev-sync/summary` | `POST /pmflow-os/projects/{project_id}/execution-handoffs` 生成/复用 `PromptPackage` 并创建 `ProgressEvent` 起点;repo 绑定仍走 dev-sync/GitHub App | `queued -> pushed -> coding -> pr_open -> testing -> synced/failed` | progress events、deliveries、repo/PR/commit 是执行证据 | 不得重造 dev-sync;PMFlow OS 只做项目内执行投影和 handoff contract,不直接执行本机命令 | | `TeamFeed / TeamInbox` | `pmflow_change_sets`、`pmflow_approvals`、`progress_events`、release/feedback 事件聚合 | 暂不新增专表,先 projection | `truth-state` | 由各业务动作写入源表 | feed item 不承载主状态 | 必须能追溯到源对象 | Phase F 才补 Role Inbox/Decision Desk 的真实模型 | ## 3. ProjectTruthState 后端投影 @@ -370,6 +370,41 @@ GET /pmflow-os/projects/{project_id}/execution-summary - 额外补齐最近 Prompt Package、活动 repo、PR/commit/test progress event。 - 返回前端 `ExecutionContext` 兼容结构与详细 dev task 列表。 +### 4.8 创建执行交接 + +```text +POST /pmflow-os/projects/{project_id}/execution-handoffs +``` + +请求: + +```json +{ + "task_id": "123", + "repo_binding_id": "456", + "runtime_target": "local_bridge", + "note": "可选交接备注" +} +``` + +行为: + +- 先复用项目成员/OWNER/REVIEWER 权限边界,再校验 `task_id` 必须属于当前 tenant/project。 +- `repo_binding_id` 可选;传入时必须属于当前 tenant/project 且为 active。未传入时允许创建 handoff draft,但 response 的 `warnings` 必须继续暴露 `repo_bindings_empty` 或等价缺口。 +- 若当前任务已有 active `PromptPackage`,复用该包并写入 latest handoff metadata;否则从真实 `Project / Task / Project Memory / RepoBinding` 生成 deterministic `PromptPackage(package_kind='execution_handoff')`,禁止硬编码 demo 文案。 +- 创建 `ProgressEvent(event_type='handoff_created', source='pmflow_os')` 作为可追踪起点,payload 记录 `runtime_target / prompt_package_id / repo_binding_id / boundary`。 +- 返回最新 `ExecutionSummaryProjection`:`execution / promptPackage / repoBindings / activeDevTasks / recentProgressEvents / recentDeliveries / warnings`。 + +错误: + +- `404 task_not_found`:任务不存在、跨 project 或跨 tenant。 +- `422 repo_binding_not_found`:传入的 repo binding 不存在、跨 project/tenant 或不是 active。 + +边界: + +- PMFlow backend 只创建 handoff 合同、PromptPackage 与 progress event,不直接执行本机命令。 +- 本地 `stdio`、宿主机配置写入和真实执行必须经过 Local Bridge、Codex、GitHub agent 或兼容 MCP host;PMFlow 后端不得写 `~/.codex/config.toml`。 + ## 5. ChangeSet 写入正式对象规则 `ChangeItem.objectType` 的提交规则: @@ -442,6 +477,7 @@ PMFlow OS 不重造开发执行系统。执行桥复用: 4. Phase C2 backend:已实现 ChangeSet atomic commit,把 `accepted / edited` 写入 `truth_nodes / tasks / semantic_facts` 并补齐高风险 approval gate。 5. Phase C2 frontend:已把真实模式下的 `promoteAcceptedToGraph` 接到 `POST /pmflow-os/change-sets/{change_set_id}/commit`,commit 后 refetch truth-state。 6. Phase D:把 `ExecutionBridgePage` 切到 `execution-summary` 和 dev-sync 真实数据。 -7. Phase E:把 Release/Growth 投影接入 release validation、feedback、revenue、ROI、compliance。 +7. Phase D.2:通过 `POST /pmflow-os/projects/{project_id}/execution-handoffs` 创建执行交接合同,生成/复用 PromptPackage 并记录 `handoff_created` progress event 起点。 +8. Phase E:把 Release/Growth 投影接入 release validation、feedback、revenue、ROI、compliance。 本文档若与后续实现不一致,必须先更新合同再改代码。 diff --git a/frontend/src/api/pmflowOs.ts b/frontend/src/api/pmflowOs.ts index c55165e4..fbd3e53d 100644 --- a/frontend/src/api/pmflowOs.ts +++ b/frontend/src/api/pmflowOs.ts @@ -14,14 +14,106 @@ export interface PMFlowExecutionSummary { projectId: string; execution: PMFlowExecutionContext; devSync: Record; - promptPackage?: Record | null; - repoBindings: Array>; - activeDevTasks: Array>; - recentProgressEvents: Array>; - recentDeliveries: Array>; + promptPackage?: PMFlowPromptPackage | null; + repoBindings: PMFlowRepoBinding[]; + activeDevTasks: PMFlowDevTask[]; + recentProgressEvents: PMFlowProgressEvent[]; + recentDeliveries: PMFlowRecentDelivery[]; warnings: string[]; } +export type PMFlowExecutionRuntimeTarget = "local_bridge" | "codex" | "github_agent" | "manual"; + +export interface PMFlowExecutionHandoffRequest { + task_id: string; + repo_binding_id?: string | null; + runtime_target: PMFlowExecutionRuntimeTarget; + note?: string | null; +} + +export interface PMFlowPromptPackage { + id: string; + project_id: string; + charter_id: string; + task_id?: string | null; + status: string; + package_kind: string; + package_text: string; + package_payload: Record; + created_at?: string | null; +} + +export interface PMFlowRepoBinding { + id: string; + project_id: string; + provider: string; + owner: string; + repo_name: string; + full_name: string; + default_branch: string; + external_repo_id?: string | null; + installation_id?: string | null; + installation_account_login?: string | null; + app_state?: string; + status: string; + task_link_count?: number; + recent_event_count?: number; +} + +export interface PMFlowDevTask { + id: string; + title: string; + description?: string | null; + status: string; + priority?: string | null; + estimated_hours?: number | null; + project_id: string; + charter_id?: string | null; + charter_title?: string | null; + assignee?: { id: string; name?: string | null; role?: string | null } | null; + prompt_package_count?: number; + model_binding?: Record | null; + code_link?: { + id: string; + status: string; + branch_name?: string | null; + pr_number?: number | null; + last_commit_sha?: string | null; + repo?: PMFlowRepoBinding | null; + } | null; +} + +export interface PMFlowProgressEvent { + id: string; + event_type: string; + source: string; + summary?: string | null; + workflow_state?: string | null; + derived_task_status?: string | null; + delivery_id?: string | null; + event_source_id?: string | null; + occurred_at?: string | null; + task?: { id: string; title?: string | null; status?: string | null; priority?: string | null }; + user?: { id: string; name?: string | null; role?: string | null } | null; + repo?: PMFlowRepoBinding | null; + branch_name?: string | null; + pr_number?: number | null; + commit_sha?: string | null; +} + +export interface PMFlowRecentDelivery { + id: string; + delivery_id: string; + event_type: string; + project_id?: string | null; + repo_binding_id?: string | null; + installation_id?: string | null; + status: string; + failure_reason?: string | null; + repository_full_name?: string | null; + received_at?: string | null; +} + export interface PMFlowThreadMessageResponse { id: string; threadId: string; @@ -74,6 +166,17 @@ export async function getPMFlowExecutionSummary(projectId: string): Promise { + const { data } = await api.post>( + `/pmflow-os/projects/${projectId}/execution-handoffs`, + payload + ); + return unwrap(data); +} + export async function createPMFlowThreadMessage( projectId: string, threadId: string, diff --git a/frontend/src/features/pmflow-os/components/ExecutionBridgeCards.tsx b/frontend/src/features/pmflow-os/components/ExecutionBridgeCards.tsx index 961696e5..ad579070 100644 --- a/frontend/src/features/pmflow-os/components/ExecutionBridgeCards.tsx +++ b/frontend/src/features/pmflow-os/components/ExecutionBridgeCards.tsx @@ -1,4 +1,7 @@ -import { Code2 } from "lucide-react"; +import type { ReactNode } from "react"; +import { AlertTriangle, Code2, Inbox } from "lucide-react"; + +import { cn } from "@/lib/utils"; export function PacketBlock({ label, @@ -43,3 +46,77 @@ export function LogCard({ ); } + +export function ExecutionEmptyState({ + title, + summary, +}: { + title: string; + summary: string; +}) { + return ( +
+
+ +
+
{title}
+
{summary}
+
+
+
+ ); +} + +export function ExecutionWarningList({ warnings }: { warnings: string[] }) { + if (!warnings.length) return null; + return ( +
+
+ + 执行桥缺口 +
+
+ {warnings.map((warning) => ( +
+ {warning} +
+ ))} +
+
+ ); +} + +export function ExecutionMetric({ + label, + value, + tone = "default", +}: { + label: string; + value: ReactNode; + tone?: "default" | "brand" | "warning"; +}) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +} + +export function FieldRow({ label, value }: { label: string; value: ReactNode }) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx b/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx index c7e6a3cc..82dadf6a 100644 --- a/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx +++ b/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx @@ -1,607 +1,546 @@ -import type { ReactNode } from "react"; import { + AlertTriangle, CheckCircle2, ChevronRight, - Circle, ClipboardCheck, Code2, Copy, - ExternalLink, - FileCode2, + GitBranch, GitPullRequest, - Maximize2, - MoreHorizontal, + PackageCheck, + RefreshCw, + Rocket, RotateCw, - Send, Server, - ShieldAlert, - Sparkles, Terminal, - X, } from "lucide-react"; -import { MiniTrend } from "@/features/pmflow-os/components/chrome"; + +import type { + PMFlowDevTask, + PMFlowExecutionSummary, + PMFlowExecutionRuntimeTarget, + PMFlowProgressEvent, + PMFlowRecentDelivery, + PMFlowRepoBinding, +} from "@/api/pmflowOs"; +import { + ExecutionEmptyState, + ExecutionMetric, + ExecutionWarningList, + FieldRow, + LogCard, + PacketBlock, +} from "@/features/pmflow-os/components/ExecutionBridgeCards"; import { usePMFlowOsStore } from "@/features/pmflow-os/store"; import { cn } from "@/lib/utils"; -const CODE_LINES = [ - " // 使用基础分页替代页面分页", - "const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({", - " queryKey: ['creations', params],", - " queryFn: ({ pageParam = null }) =>", - " creationsService.list({ cursor: pageParam, limit: 20, ...params }),", - " getNextPageParam: (lastPage) => lastPage.nextCursor,", - "});", - "", - "// 预加载下一页", - "useEffect(() => {", - " if (hasNextPage && isFetchingNextPage) {", - " const observer = new IntersectionObserver((entries) => {", - " if (entries[0].isIntersecting) {", - " fetchNextPage();", - " }", - " }, { threshold: 0.1 });", - " if (loadMoreRef.current) observer.observe(loadMoreRef.current);", - " return () => observer.disconnect();", - " }", - "}, [hasNextPage, isFetchingNextPage]);", -]; - -const FILE_TABS = ["CreationList.tsx", "useInfiniteData.ts", "creations.service.ts"]; +const STATUS_LABEL: Record = { + queued: "待推送", + pushed: "已推送", + coding: "开发中", + pr_open: "PR 审查中", + testing: "测试中", + synced: "已回写", + failed: "失败", +}; export default function ExecutionBridgePage() { - const { state, advanceExecution, writeBackExecution } = usePMFlowOsStore(); - const task = state.tasks.find((entry) => entry.id === state.execution.taskId) || state.tasks[0] || null; - const requirement = - state.requirements.find((entry) => entry.linkedTaskIds.includes(task?.id || "")) || - state.requirements.find((entry) => entry.id === task?.requirementId) || - null; - const risk = state.risks[0] || null; - const modelDecision = state.modelDecisions[0] || null; + const { + state, + dataSourceMode, + executionSummary, + executionSummaryStatus, + executionSummaryError, + refetchExecutionSummary, + createExecutionHandoff, + advanceExecution, + } = usePMFlowOsStore(); + const demoSummary = dataSourceMode === "demo" ? createDemoExecutionSummary(state) : null; + const summary = executionSummary || demoSummary; + const isDemo = dataSourceMode === "demo"; + const isLoading = dataSourceMode === "api" && executionSummaryStatus === "loading"; + const error = dataSourceMode === "api" ? executionSummaryError : null; return (
-
-

开发执行联动

-
- 项目上下文包 - - 开发工具执行 - - Git/PR/测试结果回写 +
+
+

开发执行联动

+
+ 项目真相层 + + Execution Summary + + Dev Sync 回流 +
+ {dataSourceMode === "api" ? ( + + ) : null}
- + {isDemo ? ( +
+ 当前为 demo 模式,仅因 URL 显式带有 ?demo=1 才使用本地演示状态。 +
+ ) : null} -
- - -
- -
-
+ {isLoading ? : null} + {error ? : null} + {!summary && !isLoading && !error ? ( + + ) : null} - + {summary ? ( + <> + + + +
+ + +
+ + + + ) : null}
); } -function ExecutionStatusStrip() { +function ExecutionStatusStrip({ summary }: { summary: PMFlowExecutionSummary }) { + const counts = summary.devSync; return (
-
- -
- - - - 执行中 - - -
- +
+ + + + + 0 ? "warning" : "default"} + />
); } -function StatusNode({ - icon: Icon, - title, - detail, - time, - tone, -}: { - icon: typeof ClipboardCheck; - title: string; - detail: string; - time: string; - tone: "brand" | "progress"; -}) { - return ( -
- - - -
-
-

{title}

- {time} -
-

{detail}

-
-
- ); -} - function ContextPackagePanel({ - taskTitle, - requirementTitle, - criteria, - modelDecision, - riskSummary, - onPush, + summary, + onCreateHandoff, + onDemoPush, }: { - taskTitle: string; - requirementTitle: string; - criteria: string[]; - modelDecision: string; - riskSummary: string; - onPush: () => void; + summary: PMFlowExecutionSummary; + onCreateHandoff?: (payload: { + task_id: string; + repo_binding_id?: string | null; + runtime_target: PMFlowExecutionRuntimeTarget; + note?: string | null; + }) => void; + onDemoPush?: () => void; }) { + const prompt = summary.promptPackage; + const handoffTask = + summary.activeDevTasks.find((task) => task.id === summary.execution.taskId) || summary.activeDevTasks[0] || null; + const handoffRepo = handoffTask?.code_link?.repo || summary.repoBindings[0] || null; + const canCreateHandoff = Boolean(handoffTask); + const handoffNextStep = !handoffTask + ? "dev-sync 没有 activeDevTasks,先把已确认任务推进到开发态。" + : !handoffRepo + ? "没有 repoBinding,将先创建执行交接草案,仓库目标需要之后补齐。" + : !prompt + ? "点击后会从真实 Project Memory / task / repo 生成 Prompt Package。" + : "点击后复用当前 active Prompt Package,并创建新的 handoff_created 起点事件。"; return ( -
); } -function CopilotSection({ title, children }: { title: string; children: ReactNode }) { +function DeliveryList({ deliveries }: { deliveries: PMFlowRecentDelivery[] }) { + if (!deliveries.length) { + return ; + } return ( -
-

{title}

- {children} +
+

近期交付

+
+ {deliveries.map((delivery) => ( +
+
+ {delivery.event_type} + {delivery.status} +
+
{delivery.repository_full_name || delivery.delivery_id}
+ {delivery.failure_reason ?
{delivery.failure_reason}
: null} +
+ ))} +
); } -function ExecutionTimeline() { - const items = [ - ["上下文包生成", "10:24", "done"], - ["推送到开发 Agent", "10:24", "done"], - ["开发执行中", "10:24 - 10:41", "active"], - ["结果回写 PMFlow", "10:42", "done"], - ] as const; +function ExecutionProgressPanel({ summary }: { summary: PMFlowExecutionSummary }) { return (
-
-

联动流水线

- +
+

进展事件

+
+ Dev Sync + +
-
-
- {items.map(([label, time, itemState], index) => ( -
- - {itemState === "active" ? : } - - - {label} - {time} - - {index < items.length - 1 ? : null} -
+ {summary.recentProgressEvents.length ? ( +
+ {summary.recentProgressEvents.map((event) => ( + ))}
-
-
执行成果趋势
-
-
-
效率 +20%
-
上下文完整度提升带动提交质量
-
- -
-
-
+ ) : ( + + )}
); } + +function ProgressEventCard({ event }: { event: PMFlowProgressEvent }) { + return ( +
+
+
+ {eventIcon(event.event_type)} + {event.event_type} +
+ {formatDateTime(event.occurred_at)} +
+

{event.summary || event.workflow_state || "事件已记录,但没有摘要。"}

+
+ + + + + +
+
+ ); +} + +function StatusPill({ status }: { status: string }) { + const failed = status === "failed"; + const done = status === "synced"; + return ( + + {done ? : failed ? : } + {STATUS_LABEL[status] || status} + + ); +} + +function eventIcon(eventType: string) { + const lower = eventType.toLowerCase(); + if (lower.includes("pr") || lower.includes("pull")) return ; + if (lower.includes("commit")) return ; + if (lower.includes("test")) return ; + if (lower.includes("deploy") || lower.includes("delivery")) return ; + return ; +} + +function formatDateTime(value?: string | null) { + if (!value) return "时间未回流"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function createDemoExecutionSummary(state: ReturnType["state"]): PMFlowExecutionSummary { + const task = state.tasks.find((entry) => entry.id === state.execution.taskId) || state.tasks[0]; + return { + projectId: state.project.id, + execution: state.execution, + devSync: { + repo_binding_count: 1, + active_task_count: task ? 1 : 0, + recent_event_count: state.feed.filter((item) => item.kind === "execution").length, + unmatched_event_count: 0, + }, + promptPackage: state.execution.prompt + ? { + id: "demo-package", + project_id: state.project.id, + charter_id: state.requirements[0]?.id || "demo-charter", + task_id: task?.id || null, + status: state.execution.status, + package_kind: "demo", + package_text: state.execution.prompt, + package_payload: {}, + created_at: null, + } + : null, + repoBindings: [ + { + id: "demo-repo", + project_id: state.project.id, + provider: "demo", + owner: "pmflow", + repo_name: "demo-workspace", + full_name: "pmflow/demo-workspace", + default_branch: "main", + status: "active", + app_state: "demo", + task_link_count: task ? 1 : 0, + recent_event_count: state.execution.commitLog.length, + }, + ], + activeDevTasks: task + ? [ + { + id: task.id, + title: task.title, + description: task.summary, + status: task.status, + priority: null, + project_id: state.project.id, + charter_id: task.requirementId || null, + charter_title: state.requirements.find((item) => item.id === task.requirementId)?.title || null, + assignee: { id: "demo-assignee", name: task.assignee, role: "demo" }, + prompt_package_count: state.execution.prompt ? 1 : 0, + code_link: null, + }, + ] + : [], + recentProgressEvents: state.execution.commitLog.map((entry, index) => ({ + id: `demo-progress-${index}`, + event_type: "demo_execution", + source: "demo", + summary: entry, + workflow_state: state.execution.status, + derived_task_status: state.execution.status, + occurred_at: null, + task: task ? { id: task.id, title: task.title, status: task.status, priority: null } : undefined, + user: null, + repo: null, + branch_name: null, + pr_number: null, + commit_sha: null, + })), + recentDeliveries: [], + warnings: state.execution.prompt ? [] : ["demo_prompt_package_empty: 当前 demo 状态尚未生成执行提示词。"], + }; +} diff --git a/frontend/src/features/pmflow-os/store.api.test.tsx b/frontend/src/features/pmflow-os/store.api.test.tsx index e01b741f..15c491a6 100644 --- a/frontend/src/features/pmflow-os/store.api.test.tsx +++ b/frontend/src/features/pmflow-os/store.api.test.tsx @@ -6,9 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { acceptPMFlowChangeItem, commitPMFlowChangeSet, + createPMFlowExecutionHandoff, createPMFlowThreadMessage, deferPMFlowChangeItem, editPMFlowChangeItem, + getPMFlowExecutionSummary, getPMFlowTruthState, } from "@/api/pmflowOs"; import { createInitialPMFlowState } from "@/features/pmflow-os/mockData"; @@ -17,17 +19,21 @@ import type { ProjectTruthState } from "@/features/pmflow-os/types"; vi.mock("@/api/pmflowOs", () => ({ getPMFlowTruthState: vi.fn(), + getPMFlowExecutionSummary: vi.fn(), createPMFlowThreadMessage: vi.fn(), createPMFlowExtraction: vi.fn(), getPMFlowChangeSet: vi.fn(), acceptPMFlowChangeItem: vi.fn(), commitPMFlowChangeSet: vi.fn(), + createPMFlowExecutionHandoff: vi.fn(), editPMFlowChangeItem: vi.fn(), deferPMFlowChangeItem: vi.fn(), })); const getTruthStateMock = vi.mocked(getPMFlowTruthState); +const getExecutionSummaryMock = vi.mocked(getPMFlowExecutionSummary); const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage); +const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff); const acceptItemMock = vi.mocked(acceptPMFlowChangeItem); const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet); const editItemMock = vi.mocked(editPMFlowChangeItem); @@ -94,6 +100,60 @@ describe("PMFlowOsProvider real change review actions", () => { vi.clearAllMocks(); window.history.pushState({}, "", "/pmflow-os?projectId=123"); getTruthStateMock.mockResolvedValue(realState()); + getExecutionSummaryMock.mockResolvedValue({ + projectId: "123", + execution: { + taskId: "task-99", + status: "coding", + prompt: "真实 prompt package", + commitLog: ["真实进度事件"], + prStatus: "pending", + testSummary: "pending", + writebackStatus: "coding", + }, + devSync: { active_task_count: 1 }, + promptPackage: { + id: "501", + project_id: "123", + charter_id: "301", + task_id: "task-99", + status: "active", + package_kind: "development", + package_text: "真实 prompt package", + package_payload: {}, + created_at: "2026-04-26T00:00:00", + }, + repoBindings: [ + { + id: "601", + project_id: "123", + provider: "github", + owner: "pmflow", + repo_name: "app", + full_name: "pmflow/app", + default_branch: "main", + status: "active", + }, + ], + activeDevTasks: [ + { + id: "task-99", + title: "真实任务", + description: "真实任务描述", + status: "confirmed", + priority: "high", + project_id: "123", + charter_id: "301", + charter_title: "真实宪章", + assignee: { id: "1", name: "Owner", role: "MEMBER" }, + prompt_package_count: 1, + code_link: null, + }, + ], + recentProgressEvents: [], + recentDeliveries: [], + warnings: [], + }); createThreadMessageMock.mockResolvedValue({ id: "41", threadId: "7", @@ -115,6 +175,55 @@ describe("PMFlowOsProvider real change review actions", () => { }); editItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); deferItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); + createExecutionHandoffMock.mockResolvedValue({ + projectId: "123", + execution: { + taskId: "task-99", + status: "pushed", + prompt: "真实 prompt package", + commitLog: ["执行交接已创建"], + prStatus: "pending", + testSummary: "pending", + writebackStatus: "pushed", + }, + devSync: { active_task_count: 1, recent_event_count: 1 }, + promptPackage: { + id: "501", + project_id: "123", + charter_id: "301", + task_id: "task-99", + status: "active", + package_kind: "execution_handoff", + package_text: "真实 prompt package", + package_payload: {}, + created_at: "2026-04-26T00:00:00", + }, + repoBindings: [ + { + id: "601", + project_id: "123", + provider: "github", + owner: "pmflow", + repo_name: "app", + full_name: "pmflow/app", + default_branch: "main", + status: "active", + }, + ], + activeDevTasks: [], + recentProgressEvents: [ + { + id: "701", + event_type: "handoff_created", + source: "pmflow_os", + summary: "执行交接已创建", + workflow_state: "pushed", + derived_task_status: "pushed", + }, + ], + recentDeliveries: [], + warnings: [], + }); }); afterEach(() => { @@ -125,6 +234,9 @@ describe("PMFlowOsProvider real change review actions", () => { const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); await waitFor(() => expect(result.current.dataSourceStatus).toBe("success")); + await waitFor(() => expect(result.current.executionSummaryStatus).toBe("success")); + expect(getExecutionSummaryMock).toHaveBeenCalledWith("123"); + expect(result.current.executionSummary?.promptPackage?.package_text).toBe("真实 prompt package"); act(() => result.current.sendMessage("继续补充")); await waitFor(() => expect(createThreadMessageMock).toHaveBeenCalledWith("123", "7", expect.objectContaining({ content: "继续补充" }))); @@ -146,6 +258,21 @@ describe("PMFlowOsProvider real change review actions", () => { ) ); + act(() => + result.current.createExecutionHandoff({ + task_id: "task-99", + repo_binding_id: "601", + runtime_target: "local_bridge", + note: "测试执行交接", + }) + ); + await waitFor(() => + expect(createExecutionHandoffMock).toHaveBeenCalledWith( + "123", + expect.objectContaining({ task_id: "task-99", repo_binding_id: "601", runtime_target: "local_bridge" }) + ) + ); + expect(result.current.state.requirements).toEqual([]); }); @@ -168,6 +295,8 @@ describe("PMFlowOsProvider real change review actions", () => { act(() => result.current.acceptChange(pending!.id)); expect(acceptItemMock).not.toHaveBeenCalled(); + expect(createExecutionHandoffMock).not.toHaveBeenCalled(); + expect(getExecutionSummaryMock).not.toHaveBeenCalled(); expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted"); }); }); diff --git a/frontend/src/features/pmflow-os/store.tsx b/frontend/src/features/pmflow-os/store.tsx index 93a1e827..267ec584 100644 --- a/frontend/src/features/pmflow-os/store.tsx +++ b/frontend/src/features/pmflow-os/store.tsx @@ -3,11 +3,15 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { acceptPMFlowChangeItem, commitPMFlowChangeSet, + createPMFlowExecutionHandoff, createPMFlowExtraction, createPMFlowThreadMessage, deferPMFlowChangeItem, editPMFlowChangeItem, + getPMFlowExecutionSummary, getPMFlowTruthState, + type PMFlowExecutionHandoffRequest, + type PMFlowExecutionSummary, type PMFlowProposedChangeItem, } from "@/api/pmflowOs"; import { createInitialPMFlowState } from "@/features/pmflow-os/mockData"; @@ -513,7 +517,11 @@ interface PMFlowOsStoreValue { dataSourceStatus: "idle" | "loading" | "success" | "error"; dataSourceError: string | null; projectionWarnings: string[]; + executionSummary: PMFlowExecutionSummary | null; + executionSummaryStatus: "idle" | "loading" | "success" | "error"; + executionSummaryError: string | null; refetchTruthState: () => void; + refetchExecutionSummary: () => void; sendMessage: (content: string) => void; generateExtraction: (proposedItems?: PMFlowProposedChangeItem[]) => void; setLoopStage: (stage: ProjectTruthState["loopStage"]) => void; @@ -523,6 +531,7 @@ interface PMFlowOsStoreValue { deferChange: (changeId: string) => void; promoteAcceptedToGraph: (changeSetId?: string | null) => void; pushTaskToExecution: (taskId: string) => void; + createExecutionHandoff: (payload: PMFlowExecutionHandoffRequest) => void; advanceExecution: () => void; writeBackExecution: (outcome: "passed" | "failed") => void; publishRelease: () => void; @@ -547,6 +556,11 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { queryFn: () => getPMFlowTruthState(dataSource.projectId || ""), enabled: dataSource.mode === "api" && Boolean(dataSource.projectId), }); + const executionSummaryQuery = useQuery({ + queryKey: ["pmflow-os", "execution-summary", dataSource.projectId], + queryFn: () => getPMFlowExecutionSummary(dataSource.projectId || ""), + enabled: dataSource.mode === "api" && Boolean(dataSource.projectId), + }); useEffect(() => { if (dataSource.mode !== "api") return; @@ -557,6 +571,17 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { setProjectionWarnings(Array.isArray(rawWarnings) ? rawWarnings : []); }, [dataSource.mode, truthStateQuery.data]); + useEffect(() => { + if (dataSource.mode !== "api") return; + if (!executionSummaryQuery.data) return; + setReadError(null); + setState((current) => ({ + ...current, + selectedTaskId: executionSummaryQuery.data.execution.taskId, + execution: executionSummaryQuery.data.execution, + })); + }, [dataSource.mode, executionSummaryQuery.data]); + useEffect(() => { if (dataSource.mode !== "api") return; if (!truthStateQuery.error) return; @@ -582,7 +607,9 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { const refetchApiState = () => { if (dataSource.mode !== "api") return; void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "truth-state", dataSource.projectId] }); + void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "execution-summary", dataSource.projectId] }); void truthStateQuery.refetch(); + void executionSummaryQuery.refetch(); }; const runApiAction = (action: () => Promise) => { if (dataSource.mode !== "api") return; @@ -604,9 +631,33 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { dataSource.mode === "empty" ? "idle" : truthStateQuery.isLoading ? "loading" : readError ? "error" : "success", dataSourceError: dataSource.mode === "empty" ? "URL 缺少 projectId,未读取真实 PMFlow OS 后端投影。" : readError, projectionWarnings, + executionSummary: dataSource.mode === "api" ? executionSummaryQuery.data ?? null : null, + executionSummaryStatus: + dataSource.mode === "empty" + ? "idle" + : dataSource.mode === "demo" + ? "success" + : executionSummaryQuery.isLoading + ? "loading" + : executionSummaryQuery.error + ? "error" + : "success", + executionSummaryError: + dataSource.mode !== "api" + ? null + : executionSummaryQuery.error instanceof Error + ? executionSummaryQuery.error.message + : executionSummaryQuery.error + ? "PMFlow OS 执行摘要读取失败" + : null, refetchTruthState: () => { if (dataSource.mode === "api") refetchApiState(); }, + refetchExecutionSummary: () => { + if (dataSource.mode !== "api") return; + void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "execution-summary", dataSource.projectId] }); + void executionSummaryQuery.refetch(); + }, sendMessage: (content) => { if (dataSource.mode === "api" && dataSource.projectId) { runApiAction(() => @@ -684,6 +735,13 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) { mutateDemoOnly((current) => promoteAcceptedToGraphMutation(current, changeSetId)); }, pushTaskToExecution: (taskId) => mutateDemoOnly((current) => pushTaskToExecutionMutation(current, taskId)), + createExecutionHandoff: (payload) => { + if (dataSource.mode === "api" && dataSource.projectId) { + runApiAction(() => createPMFlowExecutionHandoff(dataSource.projectId || "", payload)); + return; + } + mutateDemoOnly((current) => pushTaskToExecutionMutation(current, payload.task_id)); + }, advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)), writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)), publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),