feat(pmflow-os): add execution handoff contract

This commit is contained in:
Albert 2026-04-26 10:20:01 +08:00
parent d5e76d67d2
commit f95bd15ff3
13 changed files with 1921 additions and 563 deletions

View File

@ -13,6 +13,7 @@ from ...schemas.pmflow_os import (
PMFlowChangeItemDeferRequest, PMFlowChangeItemDeferRequest,
PMFlowChangeItemEditRequest, PMFlowChangeItemEditRequest,
PMFlowChangeSetCommitRequest, PMFlowChangeSetCommitRequest,
PMFlowExecutionHandoffRequest,
PMFlowExtractionCreateRequest, PMFlowExtractionCreateRequest,
PMFlowThreadMessageCreateRequest, PMFlowThreadMessageCreateRequest,
) )
@ -248,3 +249,36 @@ async def get_pmflow_os_execution_summary(
raise HTTPException(status_code=404, detail="project_not_found") from exc raise HTTPException(status_code=404, detail="project_not_found") from exc
raise raise
return success_response(payload.model_dump(mode="json")) 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"))

View File

@ -12,6 +12,7 @@ PMFlowObjectStatus = Literal["draft", "confirmed", "in_progress", "blocked", "do
PMFlowRiskLevel = Literal["low", "medium", "high"] PMFlowRiskLevel = Literal["low", "medium", "high"]
PMFlowExecutionStatus = Literal["queued", "pushed", "coding", "pr_open", "testing", "synced", "failed"] PMFlowExecutionStatus = Literal["queued", "pushed", "coding", "pr_open", "testing", "synced", "failed"]
PMFlowReleaseStatus = Literal["check_pending", "ready", "shipping", "live", "watch"] 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"] ProjectLoopStage = Literal["command_idle", "ai_extracting", "extraction_ready", "review_open", "writeback_success"]
PMFlowChangeGroup = Literal["new_object", "field_update", "risk", "decision"] PMFlowChangeGroup = Literal["new_object", "field_update", "risk", "decision"]
PMFlowChangeStatus = Literal["pending_review", "accepted", "edited", "deferred", "committed", "skipped"] PMFlowChangeStatus = Literal["pending_review", "accepted", "edited", "deferred", "committed", "skipped"]
@ -86,6 +87,13 @@ class PMFlowChangeSetCommitRequest(BaseModel):
note: Optional[str] = None 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): class PMFlowChangeSetDetailResponse(BaseModel):
changeSet: dict[str, Any] changeSet: dict[str, Any]
items: list[dict[str, Any]] = Field(default_factory=list) items: list[dict[str, Any]] = Field(default_factory=list)

View File

@ -25,6 +25,7 @@ from ..models import (
ProjectModelBinding, ProjectModelBinding,
PromptPackage, PromptPackage,
ReleaseValidationRun, ReleaseValidationRun,
RepoBinding,
RevenueEvent, RevenueEvent,
ROISnapshot, ROISnapshot,
SemanticFact, SemanticFact,
@ -35,6 +36,7 @@ from ..models import (
from ..schemas.pmflow_os import ( from ..schemas.pmflow_os import (
ExecutionSummaryProjection, ExecutionSummaryProjection,
PMFlowDecision, PMFlowDecision,
PMFlowExecutionHandoffRequest,
PMFlowExecutionContext, PMFlowExecutionContext,
PMFlowFeedEvent, PMFlowFeedEvent,
PMFlowMetricSignal, PMFlowMetricSignal,
@ -51,6 +53,7 @@ from ..schemas.pmflow_os import (
) )
from .ai_project_control_service import AIProjectControlService from .ai_project_control_service import AIProjectControlService
from .pmflow_change_review_service import PMFlowChangeReviewService from .pmflow_change_review_service import PMFlowChangeReviewService
from ..utils.timezone import now_cn_naive
class PMFlowOSProjectionService: class PMFlowOSProjectionService:
@ -206,6 +209,59 @@ class PMFlowOSProjectionService:
warnings=warnings, 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: async def _load_project(self, *, project_id: int) -> Project:
result = await self.db.execute( result = await self.db.execute(
select(Project).where(and_(Project.id == int(project_id), Project.tenant_id == self.tenant_id)) 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()] 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]: async def _load_latest_prompt_package(self, *, project_id: int) -> Optional[PromptPackage]:
result = await self.db.execute( result = await self.db.execute(
select(PromptPackage) select(PromptPackage)
@ -241,6 +338,73 @@ class PMFlowOSProjectionService:
) )
return result.scalar_one_or_none() 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]]: async def _load_project_thread(self, *, project_id: int) -> tuple[Optional[PMFlowThreadModel], list[PMFlowThreadMessage]]:
thread_result = await self.db.execute( thread_result = await self.db.execute(
select(PMFlowThreadModel) select(PMFlowThreadModel)
@ -954,6 +1118,153 @@ class PMFlowOSProjectionService:
"created_at": package.created_at.isoformat() if package.created_at else None, "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]: def _latest_iso(self, values: list[Optional[datetime]]) -> Optional[str]:
valid = [value for value in values if value is not None] valid = [value for value in values if value is not None]
if not valid: if not valid:

View File

@ -1,13 +1,14 @@
from __future__ import annotations from __future__ import annotations
import os import os
from datetime import datetime
from types import SimpleNamespace from types import SimpleNamespace
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from fastapi import FastAPI from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from sqlalchemy import JSON from sqlalchemy import JSON, select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test") 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"] == [] 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 @pytest.mark.asyncio
async def test_project_permission_and_tenant_boundary(db_env) -> None: async def test_project_permission_and_tenant_boundary(db_env) -> None:
session = db_env session = db_env

View File

@ -91,7 +91,7 @@
## 2. 下一步推进方向 ## 2. 下一步推进方向
下一步不要扩大战线,不要新增“大模块”。正确方向是: 下一步不要扩大战线,不要新增新的战略产品表面;可以在已确认的 `Main Flow / Space / Account Settings` 这些既有表面内并行推进。正确方向是:
**把 V1 主链真实闭环化。** **把 V1 主链真实闭环化。**
@ -345,7 +345,7 @@
### 4.4 Codex 并行推进协议 ### 4.4 Codex 并行推进协议
PMFlow 允许多个 Codex 线程并行推进,但并行单位必须是明确的 `Packet`,不能是“一个大模块”或“一个方向”。没有 `write_scope` 的任务不允许并行执行。 PMFlow 允许多个 Codex 线程并行推进。产品上可以按“大模块 lane”长期并行,例如主流程、空间、个人中心,但实际执行单位仍必须是明确的 `Packet`。不能只用“做空间”“做个人中心”“做主流程”这类模糊大模块直接开工;每个模块 lane 都必须拆成带 `write_scope` 的 Packet。没有 `write_scope` 的任务不允许并行执行。
每个并行 `Packet` 开始前必须声明: 每个并行 `Packet` 开始前必须声明:
@ -370,6 +370,167 @@ PMFlow 允许多个 Codex 线程并行推进,但并行单位必须是明确的
- 并行线程不得在自己的 `write_scope` 外做“顺手修复”;发现阻塞问题时,只记录到 integration notes 或新建后续 Packet。 - 并行线程不得在自己的 `write_scope` 外做“顺手修复”;发现阻塞问题时,只记录到 integration notes 或新建后续 Packet。
- 同一批并行开发必须指定一个 `integration owner`,负责最终合并共享文件、跑集成验证和更新进度台账。 - 同一批并行开发必须指定一个 `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=<id>` 真实项目上下文 | 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 默认并行切片 ### 4.5 V1 Productized Loop 默认并行切片
V1 主链并行开发默认按 lane 切分: 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 已把项目内消息、结构化候选、审查动作和证据链落到真实表。 - Phase C1 已把项目内消息、结构化候选、审查动作和证据链落到真实表。
- C2-contract 已锁定 commit API、原子提交规则、响应合同和失败语义。 - C2-contract 已锁定 commit API、原子提交规则、响应合同和失败语义。
- C2-backend 已按合同实现 atomic commit route/service/schema/tests,C2-frontend 已把真实模式写入接到 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 和下一步。
输出: 默认写范围:
- 一张合同映射表。 - `frontend/src/features/pmflow-os/`
- 明确复用 `TruthNode / Task / semantic facts / ai_project_control` 的边界。 - `frontend/src/api/pmflowOs.ts`
- 明确是否需要新增 `pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`。 - `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。 ##### Account Settings Packet:`account-settings-v1`
- API 路由设计。
- API response envelope。
#### A3:最小真实读路径
目标: 目标:
先让 `CommandCenter / ProjectMemory / WorkGraph` 从真实后端读取一个项目的摘要和正式对象。 - 建设 `/me` 和个人设置首版。
- 覆盖个人资料、AI 偏好、通知偏好、账户操作和移动端 bottom sheet。
- 不改 OAuth/JWT/auth core,不改项目主链。
输出: 默认写范围:
- 前端 API 封装。 - `frontend/src/features/account-settings/`(建议新建)
- React Query 接入。 - `frontend/src/features/pmflow-os/pages/AccountPage.tsx`
- 本地 mock 降为 fallback/demo,不再是正式路径。 - `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. 进度台账 ## 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=<id>`。
- 待确认项只读取 ChangeSet / approvals 摘要,点击进入稳定主流程 `/change-review?projectId=<id>&changeSetId=<id>`,不直接执行 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 ### 2026-04-26 / Phase C2 Frontend - ChangeSet Commit Wiring / done
完成内容: 完成内容:

View File

@ -23,7 +23,7 @@
- `docs/INBOX_INLINE_OPS_3_PHASE_CHECKLIST.md`:消息台内联操作执行清单 - `docs/INBOX_INLINE_OPS_3_PHASE_CHECKLIST.md`:消息台内联操作执行清单
- `docs/architecture/MASTER_CONTRACT.md`:跨栈主契约 - `docs/architecture/MASTER_CONTRACT.md`:跨栈主契约
- `docs/architecture/CODE_REFERENCE_INDEX.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/RUNTIME_GUARDRAILS.md`:运行边界、守门规则与已知运行假设
- `docs/architecture/API_CONTRACTS.md`:接口/Schema 契约 - `docs/architecture/API_CONTRACTS.md`:接口/Schema 契约
- `docs/architecture/DATAFLOW.md`:主链与数据流说明 - `docs/architecture/DATAFLOW.md`:主链与数据流说明
@ -172,6 +172,7 @@
- `frontend/src/components/login/` - `frontend/src/components/login/`
- `frontend/src/features/pmflow-os/pages/` - `frontend/src/features/pmflow-os/pages/`
- `frontend/src/features/pmflow-os/components/` - `frontend/src/features/pmflow-os/components/`
- `frontend/src/features/account-settings/`
- `frontend/src/features/personal-space/components/` - `frontend/src/features/personal-space/components/`
- `frontend/src/pages/skill-tasks/` - `frontend/src/pages/skill-tasks/`
- `frontend/src/pages/skills/` - `frontend/src/pages/skills/`
@ -192,12 +193,13 @@
- `frontend/src/components/ai-steward/QAResponseMetaCard.test.tsx` - `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` 锁定。 - 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`。 - 登录页与营销页仍共享 `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=<id>`,只有显式 `?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`。 - 前端主包治理已切到“同步首屏最小集 + 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=<id>`,只有显式 `?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/mockData.ts` 与 `PersonalSpacePage.tsx` 是空间首页本地真相和视图层;移动端为空间首页独立 App 布局,不直接压缩桌面卡片;`frontend/src/pages/PersonalSpacePage.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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`;暂存想法后端只读聚合尚未接入时只显示 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/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=<id>` 时通过 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/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` 仍保留演示用项目内 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` 表达“未确认不得写入”的局部提示。 - `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=<id>` 时通过 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 成功。
- 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/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。 - 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/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、治理页和图表代码。 - `frontend/vite.config.ts` 现补了固定 vendor chunk 分组(`framework / state-query / ui-radix / icons / charts / markdown`)与业务 chunk 分组(`ai-steward / sstc / governance`),目标是让登录页与基础导航不再背 AI Steward、SSTC、治理页和图表代码。

View File

@ -89,27 +89,30 @@
- SSTC 负责人推荐接口:参考 `POST /projects/{project_id}/tasks/{task_id}/recommend-assignee` - SSTC 负责人推荐接口:参考 `POST /projects/{project_id}/tasks/{task_id}/recommend-assignee`
- SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}` - SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}`
- SSTC 发布创建接口:参考 `POST /projects/{project_id}/releases` - 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` - 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` - 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. 前端参考 ## 3. 前端参考
- PMFlow UI 契约真源:先读 `docs/PMFLOW_UI_CONTRACT.md`,再修改 `project-launch`、`personal-space`、`pmflow-os` 相关页面;该文档锁定 Logo、色彩、移动端底部导航、状态标签、AI 操作/用户动作/待确认视觉语法和核心功能 canonical 契约 - 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 草案 - 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/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 问答浮窗与上下文显性化组件:参考 `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 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 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` - AI Steward / QA session 前端 contract:参考 `frontend/src/api/aiSteward.ts`
- PMFlow OS 前端 MVP 产品域:参考 `frontend/src/features/pmflow-os/` - 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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`;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` - 新项目启动台 / 首次进入页前端产品域:参考 `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=<id>` 走 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=<id>` 走 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/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/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` - 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` - 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` - 网页问答显式锚点与 AI Steward URL 构造:参考 `frontend/src/components/ai-steward/qaPageAnchor.ts`
- Inbox 页面入口:参考 `frontend/src/pages/inbox/` - Inbox 页面入口:参考 `frontend/src/pages/inbox/`
- Inbox `plan_mode` 展开卡片与映射:参考 `frontend/src/components/inbox/InboxListItem.tsx`、`frontend/src/lib/inbox.ts`、`frontend/src/api/aiPmo.ts` - Inbox `plan_mode` 展开卡片与映射:参考 `frontend/src/components/inbox/InboxListItem.tsx`、`frontend/src/lib/inbox.ts`、`frontend/src/api/aiPmo.ts`

View File

@ -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 先按投影聚合 | | `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 中分清 | | `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` | 指标来源和采样窗口必须展示 | 只做轻量反馈,不引入完整财务系统 | | `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 的真实模型 | | `TeamFeed / TeamInbox` | `pmflow_change_sets`、`pmflow_approvals`、`progress_events`、release/feedback 事件聚合 | 暂不新增专表,先 projection | `truth-state` | 由各业务动作写入源表 | feed item 不承载主状态 | 必须能追溯到源对象 | Phase F 才补 Role Inbox/Decision Desk 的真实模型 |
## 3. ProjectTruthState 后端投影 ## 3. ProjectTruthState 后端投影
@ -370,6 +370,41 @@ GET /pmflow-os/projects/{project_id}/execution-summary
- 额外补齐最近 Prompt Package、活动 repo、PR/commit/test progress event。 - 额外补齐最近 Prompt Package、活动 repo、PR/commit/test progress event。
- 返回前端 `ExecutionContext` 兼容结构与详细 dev task 列表。 - 返回前端 `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 写入正式对象规则 ## 5. ChangeSet 写入正式对象规则
`ChangeItem.objectType` 的提交规则: `ChangeItem.objectType` 的提交规则:
@ -442,6 +477,7 @@ PMFlow OS 不重造开发执行系统。执行桥复用:
4. Phase C2 backend:已实现 ChangeSet atomic commit,把 `accepted / edited` 写入 `truth_nodes / tasks / semantic_facts` 并补齐高风险 approval gate。 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。 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 真实数据。 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。
本文档若与后续实现不一致,必须先更新合同再改代码。 本文档若与后续实现不一致,必须先更新合同再改代码。

View File

@ -14,14 +14,106 @@ export interface PMFlowExecutionSummary {
projectId: string; projectId: string;
execution: PMFlowExecutionContext; execution: PMFlowExecutionContext;
devSync: Record<string, unknown>; devSync: Record<string, unknown>;
promptPackage?: Record<string, unknown> | null; promptPackage?: PMFlowPromptPackage | null;
repoBindings: Array<Record<string, unknown>>; repoBindings: PMFlowRepoBinding[];
activeDevTasks: Array<Record<string, unknown>>; activeDevTasks: PMFlowDevTask[];
recentProgressEvents: Array<Record<string, unknown>>; recentProgressEvents: PMFlowProgressEvent[];
recentDeliveries: Array<Record<string, unknown>>; recentDeliveries: PMFlowRecentDelivery[];
warnings: string[]; 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<string, unknown>;
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<string, unknown> | 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 { export interface PMFlowThreadMessageResponse {
id: string; id: string;
threadId: string; threadId: string;
@ -74,6 +166,17 @@ export async function getPMFlowExecutionSummary(projectId: string): Promise<PMFl
return unwrap(data); return unwrap(data);
} }
export async function createPMFlowExecutionHandoff(
projectId: string,
payload: PMFlowExecutionHandoffRequest
): Promise<PMFlowExecutionSummary> {
const { data } = await api.post<ApiEnvelope<PMFlowExecutionSummary>>(
`/pmflow-os/projects/${projectId}/execution-handoffs`,
payload
);
return unwrap(data);
}
export async function createPMFlowThreadMessage( export async function createPMFlowThreadMessage(
projectId: string, projectId: string,
threadId: string, threadId: string,

View File

@ -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({ export function PacketBlock({
label, label,
@ -43,3 +46,77 @@ export function LogCard({
</div> </div>
); );
} }
export function ExecutionEmptyState({
title,
summary,
}: {
title: string;
summary: string;
}) {
return (
<div className="rounded-xl border border-dashed border-[#d8e3f4] bg-[#fbfdff] p-4 text-sm">
<div className="flex items-start gap-3">
<Inbox className="mt-0.5 h-4 w-4 shrink-0 text-slate-400" />
<div>
<div className="font-semibold text-slate-700">{title}</div>
<div className="mt-1 leading-6 text-slate-500">{summary}</div>
</div>
</div>
</div>
);
}
export function ExecutionWarningList({ warnings }: { warnings: string[] }) {
if (!warnings.length) return null;
return (
<section className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3">
<div className="flex items-center gap-2 text-sm font-semibold text-amber-800">
<AlertTriangle className="h-4 w-4" />
执行桥缺口
</div>
<div className="mt-2 space-y-1">
{warnings.map((warning) => (
<div key={warning} className="text-xs leading-5 text-amber-700">
{warning}
</div>
))}
</div>
</section>
);
}
export function ExecutionMetric({
label,
value,
tone = "default",
}: {
label: string;
value: ReactNode;
tone?: "default" | "brand" | "warning";
}) {
return (
<div className="rounded-xl border border-[#e8eef8] bg-white px-4 py-3">
<div className="text-xs font-medium text-slate-500">{label}</div>
<div
className={cn(
"mt-2 text-lg font-semibold",
tone === "brand" && "text-[#2463EB]",
tone === "warning" && "text-amber-700",
tone === "default" && "text-slate-950"
)}
>
{value}
</div>
</div>
);
}
export function FieldRow({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="grid grid-cols-[92px_minmax(0,1fr)] gap-3 text-sm">
<dt className="text-slate-400">{label}</dt>
<dd className="min-w-0 font-medium text-slate-700">{value}</dd>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { import {
acceptPMFlowChangeItem, acceptPMFlowChangeItem,
commitPMFlowChangeSet, commitPMFlowChangeSet,
createPMFlowExecutionHandoff,
createPMFlowThreadMessage, createPMFlowThreadMessage,
deferPMFlowChangeItem, deferPMFlowChangeItem,
editPMFlowChangeItem, editPMFlowChangeItem,
getPMFlowExecutionSummary,
getPMFlowTruthState, getPMFlowTruthState,
} from "@/api/pmflowOs"; } from "@/api/pmflowOs";
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData"; import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
@ -17,17 +19,21 @@ import type { ProjectTruthState } from "@/features/pmflow-os/types";
vi.mock("@/api/pmflowOs", () => ({ vi.mock("@/api/pmflowOs", () => ({
getPMFlowTruthState: vi.fn(), getPMFlowTruthState: vi.fn(),
getPMFlowExecutionSummary: vi.fn(),
createPMFlowThreadMessage: vi.fn(), createPMFlowThreadMessage: vi.fn(),
createPMFlowExtraction: vi.fn(), createPMFlowExtraction: vi.fn(),
getPMFlowChangeSet: vi.fn(), getPMFlowChangeSet: vi.fn(),
acceptPMFlowChangeItem: vi.fn(), acceptPMFlowChangeItem: vi.fn(),
commitPMFlowChangeSet: vi.fn(), commitPMFlowChangeSet: vi.fn(),
createPMFlowExecutionHandoff: vi.fn(),
editPMFlowChangeItem: vi.fn(), editPMFlowChangeItem: vi.fn(),
deferPMFlowChangeItem: vi.fn(), deferPMFlowChangeItem: vi.fn(),
})); }));
const getTruthStateMock = vi.mocked(getPMFlowTruthState); const getTruthStateMock = vi.mocked(getPMFlowTruthState);
const getExecutionSummaryMock = vi.mocked(getPMFlowExecutionSummary);
const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage); const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage);
const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
const acceptItemMock = vi.mocked(acceptPMFlowChangeItem); const acceptItemMock = vi.mocked(acceptPMFlowChangeItem);
const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet); const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet);
const editItemMock = vi.mocked(editPMFlowChangeItem); const editItemMock = vi.mocked(editPMFlowChangeItem);
@ -94,6 +100,60 @@ describe("PMFlowOsProvider real change review actions", () => {
vi.clearAllMocks(); vi.clearAllMocks();
window.history.pushState({}, "", "/pmflow-os?projectId=123"); window.history.pushState({}, "", "/pmflow-os?projectId=123");
getTruthStateMock.mockResolvedValue(realState()); 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({ createThreadMessageMock.mockResolvedValue({
id: "41", id: "41",
threadId: "7", threadId: "7",
@ -115,6 +175,55 @@ describe("PMFlowOsProvider real change review actions", () => {
}); });
editItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); editItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] });
deferItemMock.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(() => { afterEach(() => {
@ -125,6 +234,9 @@ describe("PMFlowOsProvider real change review actions", () => {
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
await waitFor(() => expect(result.current.dataSourceStatus).toBe("success")); 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("继续补充")); act(() => result.current.sendMessage("继续补充"));
await waitFor(() => expect(createThreadMessageMock).toHaveBeenCalledWith("123", "7", expect.objectContaining({ content: "继续补充" }))); 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([]); expect(result.current.state.requirements).toEqual([]);
}); });
@ -168,6 +295,8 @@ describe("PMFlowOsProvider real change review actions", () => {
act(() => result.current.acceptChange(pending!.id)); act(() => result.current.acceptChange(pending!.id));
expect(acceptItemMock).not.toHaveBeenCalled(); 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"); expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted");
}); });
}); });

View File

@ -3,11 +3,15 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
acceptPMFlowChangeItem, acceptPMFlowChangeItem,
commitPMFlowChangeSet, commitPMFlowChangeSet,
createPMFlowExecutionHandoff,
createPMFlowExtraction, createPMFlowExtraction,
createPMFlowThreadMessage, createPMFlowThreadMessage,
deferPMFlowChangeItem, deferPMFlowChangeItem,
editPMFlowChangeItem, editPMFlowChangeItem,
getPMFlowExecutionSummary,
getPMFlowTruthState, getPMFlowTruthState,
type PMFlowExecutionHandoffRequest,
type PMFlowExecutionSummary,
type PMFlowProposedChangeItem, type PMFlowProposedChangeItem,
} from "@/api/pmflowOs"; } from "@/api/pmflowOs";
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData"; import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
@ -513,7 +517,11 @@ interface PMFlowOsStoreValue {
dataSourceStatus: "idle" | "loading" | "success" | "error"; dataSourceStatus: "idle" | "loading" | "success" | "error";
dataSourceError: string | null; dataSourceError: string | null;
projectionWarnings: string[]; projectionWarnings: string[];
executionSummary: PMFlowExecutionSummary | null;
executionSummaryStatus: "idle" | "loading" | "success" | "error";
executionSummaryError: string | null;
refetchTruthState: () => void; refetchTruthState: () => void;
refetchExecutionSummary: () => void;
sendMessage: (content: string) => void; sendMessage: (content: string) => void;
generateExtraction: (proposedItems?: PMFlowProposedChangeItem[]) => void; generateExtraction: (proposedItems?: PMFlowProposedChangeItem[]) => void;
setLoopStage: (stage: ProjectTruthState["loopStage"]) => void; setLoopStage: (stage: ProjectTruthState["loopStage"]) => void;
@ -523,6 +531,7 @@ interface PMFlowOsStoreValue {
deferChange: (changeId: string) => void; deferChange: (changeId: string) => void;
promoteAcceptedToGraph: (changeSetId?: string | null) => void; promoteAcceptedToGraph: (changeSetId?: string | null) => void;
pushTaskToExecution: (taskId: string) => void; pushTaskToExecution: (taskId: string) => void;
createExecutionHandoff: (payload: PMFlowExecutionHandoffRequest) => void;
advanceExecution: () => void; advanceExecution: () => void;
writeBackExecution: (outcome: "passed" | "failed") => void; writeBackExecution: (outcome: "passed" | "failed") => void;
publishRelease: () => void; publishRelease: () => void;
@ -547,6 +556,11 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
queryFn: () => getPMFlowTruthState(dataSource.projectId || ""), queryFn: () => getPMFlowTruthState(dataSource.projectId || ""),
enabled: dataSource.mode === "api" && Boolean(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(() => { useEffect(() => {
if (dataSource.mode !== "api") return; if (dataSource.mode !== "api") return;
@ -557,6 +571,17 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
setProjectionWarnings(Array.isArray(rawWarnings) ? rawWarnings : []); setProjectionWarnings(Array.isArray(rawWarnings) ? rawWarnings : []);
}, [dataSource.mode, truthStateQuery.data]); }, [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(() => { useEffect(() => {
if (dataSource.mode !== "api") return; if (dataSource.mode !== "api") return;
if (!truthStateQuery.error) return; if (!truthStateQuery.error) return;
@ -582,7 +607,9 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
const refetchApiState = () => { const refetchApiState = () => {
if (dataSource.mode !== "api") return; if (dataSource.mode !== "api") return;
void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "truth-state", dataSource.projectId] }); void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "truth-state", dataSource.projectId] });
void queryClient.invalidateQueries({ queryKey: ["pmflow-os", "execution-summary", dataSource.projectId] });
void truthStateQuery.refetch(); void truthStateQuery.refetch();
void executionSummaryQuery.refetch();
}; };
const runApiAction = (action: () => Promise<unknown>) => { const runApiAction = (action: () => Promise<unknown>) => {
if (dataSource.mode !== "api") return; 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", dataSource.mode === "empty" ? "idle" : truthStateQuery.isLoading ? "loading" : readError ? "error" : "success",
dataSourceError: dataSource.mode === "empty" ? "URL 缺少 projectId,未读取真实 PMFlow OS 后端投影。" : readError, dataSourceError: dataSource.mode === "empty" ? "URL 缺少 projectId,未读取真实 PMFlow OS 后端投影。" : readError,
projectionWarnings, 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: () => { refetchTruthState: () => {
if (dataSource.mode === "api") refetchApiState(); 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) => { sendMessage: (content) => {
if (dataSource.mode === "api" && dataSource.projectId) { if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() => runApiAction(() =>
@ -684,6 +735,13 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
mutateDemoOnly((current) => promoteAcceptedToGraphMutation(current, changeSetId)); mutateDemoOnly((current) => promoteAcceptedToGraphMutation(current, changeSetId));
}, },
pushTaskToExecution: (taskId) => mutateDemoOnly((current) => pushTaskToExecutionMutation(current, taskId)), 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)), advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)),
writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)), writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)),
publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)), publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),