feat(pmflow-os): add execution handoff contract
This commit is contained in:
parent
d5e76d67d2
commit
f95bd15ff3
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
完成内容:
|
完成内容:
|
||||||
|
|
|
||||||
|
|
@ -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、治理页和图表代码。
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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。
|
||||||
|
|
||||||
本文档若与后续实现不一致,必须先更新合同再改代码。
|
本文档若与后续实现不一致,必须先更新合同再改代码。
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue