feat(pmflow-os): consume execution handoffs
This commit is contained in:
parent
4b2193e828
commit
2956906f19
|
|
@ -134,7 +134,7 @@ frontend/
|
||||||
- `frontend/src/features/project-launch/` 是新项目启动台独立产品域:`types.ts / useProjectLaunchState.ts` 承载状态合同和真实 API 编排,`mockData.ts` 只在显式 demo 模式下使用;默认路径调用 `POST /pmflow-launch/drafts` 生成可审查草案,确认后调用 `POST /pmflow-launch/drafts/{draft_id}/confirm` 创建真实项目并跳转 `/pmflow-os?projectId=<id>`。`LaunchShell` 桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,主画布仍由 `IntentInputPanel / AIProgressCanvas / HumanReviewCanvas / DraftEditCanvas / FailureStatePanel / SuccessTransitionPanel` 承接,移动端仍由 `MobileLaunchLayout` 承接;`frontend/src/pages/ProjectLaunchPage.tsx` 仅做薄入口。
|
- `frontend/src/features/project-launch/` 是新项目启动台独立产品域:`types.ts / useProjectLaunchState.ts` 承载状态合同和真实 API 编排,`mockData.ts` 只在显式 demo 模式下使用;默认路径调用 `POST /pmflow-launch/drafts` 生成可审查草案,确认后调用 `POST /pmflow-launch/drafts/{draft_id}/confirm` 创建真实项目并跳转 `/pmflow-os?projectId=<id>`。`LaunchShell` 桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,主画布仍由 `IntentInputPanel / AIProgressCanvas / HumanReviewCanvas / DraftEditCanvas / FailureStatePanel / SuccessTransitionPanel` 承接,移动端仍由 `MobileLaunchLayout` 承接;`frontend/src/pages/ProjectLaunchPage.tsx` 仅做薄入口。
|
||||||
- `frontend/src/features/personal-space/` 是空间层入口:`PersonalSpacePage.tsx` 承接桌面个人工作面与独立移动单手布局,`frontend/src/api/space.ts` 只读聚合 `projects` summary、PMFlow OS `truth-state`、`execution-summary` 并生成继续推进、待确认、AI 正在推进、草案箱、最近项目摘要;点击项目/确认/草案只跳回 `/pmflow-os?projectId=<id>` 主流程处理,不在 Space 写 Project Memory 或 execution event。`components/SpaceChrome.tsx` 承接空间层左侧栏、顶部条与空间用量组件,并被 `/project-launch` 桌面首次新建壳复用;`frontend/src/pages/PersonalSpacePage.tsx` 仅做薄入口。
|
- `frontend/src/features/personal-space/` 是空间层入口:`PersonalSpacePage.tsx` 承接桌面个人工作面与独立移动单手布局,`frontend/src/api/space.ts` 只读聚合 `projects` summary、PMFlow OS `truth-state`、`execution-summary` 并生成继续推进、待确认、AI 正在推进、草案箱、最近项目摘要;点击项目/确认/草案只跳回 `/pmflow-os?projectId=<id>` 主流程处理,不在 Space 写 Project Memory 或 execution event。`components/SpaceChrome.tsx` 承接空间层左侧栏、顶部条与空间用量组件,并被 `/project-launch` 桌面首次新建壳复用;`frontend/src/pages/PersonalSpacePage.tsx` 仅做薄入口。
|
||||||
- `frontend/src/features/account-settings/` 是 Account Settings 独立产品域:`AccountSettingsPanel.tsx` 承接 `/me` 桌面设置页、移动端分组列表与 bottom sheet,`frontend/src/api/accountSettings.ts` 定义个人资料、默认语言、AI 回答深度、证据展示偏好和四类通知偏好合同,当前首版使用浏览器本地持久化作为前端合同落点,不改 OAuth/JWT/auth core;`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 仅做薄入口并复用既有退出登录 bottom sheet。
|
- `frontend/src/features/account-settings/` 是 Account Settings 独立产品域:`AccountSettingsPanel.tsx` 承接 `/me` 桌面设置页、移动端分组列表与 bottom sheet,`frontend/src/api/accountSettings.ts` 定义个人资料、默认语言、AI 回答深度、证据展示偏好和四类通知偏好合同,当前首版使用浏览器本地持久化作为前端合同落点,不改 OAuth/JWT/auth core;`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 仅做薄入口并复用既有退出登录 bottom sheet。
|
||||||
- `frontend/src/features/pmflow-os/` 当前承载新的对话驱动前端产品域:`store.tsx` 提供共享 truth store;真实模式通过 `?projectId=<id>` 调用 `frontend/src/api/pmflowOs.ts` 读取后端 `ProjectTruthState` projection,并调用真实 message、`proposed_items` extraction、ChangeItem `accept/edit/defer` 与 ChangeSet `commit` API,成功后 refetch truth-state;显式 `?demo=1` 才启用 `command_idle -> ai_extracting -> extraction_ready -> review_open -> writeback_success` 本地演示状态机,以及本地 `promoteAcceptedToGraph` 等演示动作。`pages/` 下包含 `CommandCenter / StructuredExtraction / ChangeReview / MemoryWritebackResult / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主页面;`ProjectMemoryPage` 在真实模式消费后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,真实模式下 accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`components/` 下除了 `ProjectStageRail / ThreadComposer / ExtractionRail / ChangeGroupPanel / ObjectGraphSection / ContextPacketPanel / ExecutionTimeline / ReleaseGatePanel / TeamFeedPanel` 之外,新增 `ProjectStructuredLoop / PMFlowOsFrame / AccountControls / FeedbackPrimitives` 等行为组件,用来承接项目指挥台、结构化提取、待确认审查、项目记忆写入结果、项目内面包屑、项目切换器、账号退出登录交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组契约化提示提醒组件;`components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`。
|
- `frontend/src/features/pmflow-os/` 当前承载新的对话驱动前端产品域:`store.tsx` 提供共享 truth store;真实模式通过 `?projectId=<id>` 调用 `frontend/src/api/pmflowOs.ts` 读取后端 `ProjectTruthState` projection,并调用真实 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet `commit`、Execution Handoff create/consume API,成功后 refetch truth-state / execution-summary;显式 `?demo=1` 才启用 `command_idle -> ai_extracting -> extraction_ready -> review_open -> writeback_success` 本地演示状态机,以及本地 `promoteAcceptedToGraph` 等演示动作。`pages/` 下包含 `CommandCenter / StructuredExtraction / ChangeReview / MemoryWritebackResult / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主页面;`ExecutionBridgePage` 已支持创建 handoff 与“运行端领取确认”,后端只记录 `handoff_created / handoff_consumed` progress event,不执行本机命令;`ProjectMemoryPage` 在真实模式消费后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,真实模式下 accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`components/` 下除了 `ProjectStageRail / ThreadComposer / ExtractionRail / ChangeGroupPanel / ObjectGraphSection / ContextPacketPanel / ExecutionTimeline / ReleaseGatePanel / TeamFeedPanel` 之外,新增 `ProjectStructuredLoop / PMFlowOsFrame / AccountControls / FeedbackPrimitives` 等行为组件,用来承接项目指挥台、结构化提取、待确认审查、项目记忆写入结果、项目内面包屑、项目切换器、账号退出登录交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组契约化提示提醒组件;`components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`。
|
||||||
- 旧登录实现 `frontend/src/components/ui/sign-in.tsx` 与 `frontend/src/components/ui/demo.tsx` 已移除,`frontend/src/components/login/` 是当前唯一登录真源。
|
- 旧登录实现 `frontend/src/components/ui/sign-in.tsx` 与 `frontend/src/components/ui/demo.tsx` 已移除,`frontend/src/components/login/` 是当前唯一登录真源。
|
||||||
|
|
||||||
### 5.1 `frontend/src/components/sstc/` 目录说明(模块化拆分后)
|
### 5.1 `frontend/src/components/sstc/` 目录说明(模块化拆分后)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from ...schemas.pmflow_os import (
|
||||||
PMFlowChangeItemDeferRequest,
|
PMFlowChangeItemDeferRequest,
|
||||||
PMFlowChangeItemEditRequest,
|
PMFlowChangeItemEditRequest,
|
||||||
PMFlowChangeSetCommitRequest,
|
PMFlowChangeSetCommitRequest,
|
||||||
|
PMFlowExecutionHandoffConsumeRequest,
|
||||||
PMFlowExecutionHandoffRequest,
|
PMFlowExecutionHandoffRequest,
|
||||||
PMFlowExtractionCreateRequest,
|
PMFlowExtractionCreateRequest,
|
||||||
PMFlowThreadMessageCreateRequest,
|
PMFlowThreadMessageCreateRequest,
|
||||||
|
|
@ -282,3 +283,40 @@ async def create_pmflow_os_execution_handoff(
|
||||||
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/{prompt_package_id}/consume")
|
||||||
|
async def consume_pmflow_os_execution_handoff(
|
||||||
|
project_id: int,
|
||||||
|
prompt_package_id: int,
|
||||||
|
body: PMFlowExecutionHandoffConsumeRequest | None = None,
|
||||||
|
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.consume_execution_handoff(
|
||||||
|
project_id=int(project_id),
|
||||||
|
prompt_package_id=int(prompt_package_id),
|
||||||
|
body=body or PMFlowExecutionHandoffConsumeRequest(),
|
||||||
|
current_user_id=int(current_user.id),
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
code = str(exc)
|
||||||
|
if code in {"prompt_package_not_found", "execution_handoff_not_found"}:
|
||||||
|
return JSONResponse(status_code=404, content=error_response(404, code))
|
||||||
|
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"))
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,12 @@ class PMFlowExecutionHandoffRequest(BaseModel):
|
||||||
note: Optional[str] = Field(default=None, max_length=4000)
|
note: Optional[str] = Field(default=None, max_length=4000)
|
||||||
|
|
||||||
|
|
||||||
|
class PMFlowExecutionHandoffConsumeRequest(BaseModel):
|
||||||
|
runtime_target: Optional[PMFlowExecutionRuntimeTarget] = None
|
||||||
|
consumer_label: Optional[str] = Field(default=None, max_length=120)
|
||||||
|
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)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from typing import Any, Optional
|
||||||
|
|
||||||
from sqlalchemy import and_, or_, select
|
from sqlalchemy import and_, or_, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
FeedbackIngest,
|
FeedbackIngest,
|
||||||
|
|
@ -36,6 +37,7 @@ from ..models import (
|
||||||
from ..schemas.pmflow_os import (
|
from ..schemas.pmflow_os import (
|
||||||
ExecutionSummaryProjection,
|
ExecutionSummaryProjection,
|
||||||
PMFlowDecision,
|
PMFlowDecision,
|
||||||
|
PMFlowExecutionHandoffConsumeRequest,
|
||||||
PMFlowExecutionHandoffRequest,
|
PMFlowExecutionHandoffRequest,
|
||||||
PMFlowExecutionContext,
|
PMFlowExecutionContext,
|
||||||
PMFlowFeedEvent,
|
PMFlowFeedEvent,
|
||||||
|
|
@ -229,7 +231,7 @@ class PMFlowOSProjectionService:
|
||||||
note=body.note,
|
note=body.note,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
payload = self._payload(prompt_package.package_payload)
|
payload = dict(self._payload(prompt_package.package_payload))
|
||||||
payload["latest_handoff"] = self._handoff_payload(
|
payload["latest_handoff"] = self._handoff_payload(
|
||||||
task=task,
|
task=task,
|
||||||
repo=repo,
|
repo=repo,
|
||||||
|
|
@ -238,6 +240,7 @@ class PMFlowOSProjectionService:
|
||||||
prompt_package_id=int(prompt_package.id),
|
prompt_package_id=int(prompt_package.id),
|
||||||
)
|
)
|
||||||
prompt_package.package_payload = payload
|
prompt_package.package_payload = payload
|
||||||
|
flag_modified(prompt_package, "package_payload")
|
||||||
|
|
||||||
event = ProgressEvent(
|
event = ProgressEvent(
|
||||||
tenant_id=self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
|
|
@ -262,6 +265,63 @@ class PMFlowOSProjectionService:
|
||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
return await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id)
|
return await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id)
|
||||||
|
|
||||||
|
async def consume_execution_handoff(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
project_id: int,
|
||||||
|
prompt_package_id: int,
|
||||||
|
body: PMFlowExecutionHandoffConsumeRequest,
|
||||||
|
current_user_id: Optional[int] = None,
|
||||||
|
) -> ExecutionSummaryProjection:
|
||||||
|
await self._load_project(project_id=project_id)
|
||||||
|
prompt_package = await self._load_execution_prompt_package(
|
||||||
|
project_id=project_id,
|
||||||
|
prompt_package_id=prompt_package_id,
|
||||||
|
)
|
||||||
|
if prompt_package.task_id is None:
|
||||||
|
raise ValueError("task_not_found")
|
||||||
|
task = await self._load_project_task(project_id=project_id, task_id=str(prompt_package.task_id))
|
||||||
|
payload = dict(self._payload(prompt_package.package_payload))
|
||||||
|
latest_handoff = dict(self._payload(payload.get("latest_handoff")))
|
||||||
|
runtime_target = body.runtime_target or str(latest_handoff.get("runtime_target") or payload.get("runtime_target") or "manual")
|
||||||
|
repo_binding_id = str(latest_handoff.get("repo_binding_id") or "").strip() or None
|
||||||
|
repo = await self._load_project_repo_binding(project_id=project_id, repo_binding_id=repo_binding_id)
|
||||||
|
consumer_label = (body.consumer_label or runtime_target or "runtime").strip()
|
||||||
|
consumed_at = now_cn_naive().isoformat()
|
||||||
|
consumed_payload = self._handoff_consumed_payload(
|
||||||
|
task=task,
|
||||||
|
repo=repo,
|
||||||
|
runtime_target=runtime_target,
|
||||||
|
note=body.note,
|
||||||
|
prompt_package_id=int(prompt_package.id),
|
||||||
|
consumer_label=consumer_label,
|
||||||
|
consumed_at=consumed_at,
|
||||||
|
)
|
||||||
|
latest_handoff.update(consumed_payload)
|
||||||
|
payload["latest_handoff"] = latest_handoff
|
||||||
|
payload.setdefault("handoff_consumptions", [])
|
||||||
|
if isinstance(payload["handoff_consumptions"], list):
|
||||||
|
payload["handoff_consumptions"] = [consumed_payload, *payload["handoff_consumptions"][:9]]
|
||||||
|
prompt_package.package_payload = payload
|
||||||
|
flag_modified(prompt_package, "package_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_consumed",
|
||||||
|
source="pmflow_os",
|
||||||
|
derived_task_status="coding",
|
||||||
|
summary=self._handoff_consumed_summary(task=task, runtime_target=runtime_target, consumer_label=consumer_label),
|
||||||
|
payload=consumed_payload,
|
||||||
|
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))
|
||||||
|
|
@ -354,6 +414,23 @@ class PMFlowOSProjectionService:
|
||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def _load_execution_prompt_package(self, *, project_id: int, prompt_package_id: int) -> PromptPackage:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(PromptPackage).where(
|
||||||
|
and_(
|
||||||
|
PromptPackage.tenant_id == self.tenant_id,
|
||||||
|
PromptPackage.project_id == int(project_id),
|
||||||
|
PromptPackage.id == int(prompt_package_id),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
prompt_package = result.scalar_one_or_none()
|
||||||
|
if prompt_package is None:
|
||||||
|
raise ValueError("prompt_package_not_found")
|
||||||
|
if prompt_package.package_kind != "execution_handoff":
|
||||||
|
raise ValueError("execution_handoff_not_found")
|
||||||
|
return prompt_package
|
||||||
|
|
||||||
async def _create_execution_prompt_package(
|
async def _create_execution_prompt_package(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|
@ -1265,6 +1342,34 @@ class PMFlowOSProjectionService:
|
||||||
target = f"{repo.owner}/{repo.repo_name}" if repo is not None else "未绑定仓库"
|
target = f"{repo.owner}/{repo.repo_name}" if repo is not None else "未绑定仓库"
|
||||||
return f"执行交接已创建:{task.title} -> {runtime_target} / {target}"
|
return f"执行交接已创建:{task.title} -> {runtime_target} / {target}"
|
||||||
|
|
||||||
|
def _handoff_consumed_payload(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
task: Task,
|
||||||
|
repo: Optional[RepoBinding],
|
||||||
|
runtime_target: str,
|
||||||
|
note: Optional[str],
|
||||||
|
prompt_package_id: int,
|
||||||
|
consumer_label: str,
|
||||||
|
consumed_at: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"workflow_state": "coding",
|
||||||
|
"handoff_state": "consumed",
|
||||||
|
"runtime_target": runtime_target,
|
||||||
|
"consumer_label": consumer_label,
|
||||||
|
"consumed_at": consumed_at,
|
||||||
|
"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 records handoff pickup only; command execution remains outside backend runtime.",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _handoff_consumed_summary(self, *, task: Task, runtime_target: str, consumer_label: str) -> str:
|
||||||
|
return f"执行交接已领取:{task.title} -> {runtime_target} / {consumer_label}"
|
||||||
|
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,86 @@ async def test_execution_handoff_without_repo_creates_draft_warning(db_env) -> N
|
||||||
assert event.payload["handoff_state"] == "draft"
|
assert event.payload["handoff_state"] == "draft"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_execution_handoff_consume_records_pickup_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": ["领取交接包后进入开发中"]},
|
||||||
|
manual_overrides=None,
|
||||||
|
title="领取执行交接包",
|
||||||
|
description="Codex 消费 PromptPackage",
|
||||||
|
required_skill_tag_ids=[],
|
||||||
|
status="confirmed",
|
||||||
|
priority="high",
|
||||||
|
)
|
||||||
|
repo = RepoBinding(
|
||||||
|
tenant_id=1,
|
||||||
|
project_id=project.id,
|
||||||
|
provider="github",
|
||||||
|
owner="pmflow",
|
||||||
|
repo_name="handoff-consume",
|
||||||
|
default_branch="main",
|
||||||
|
installation_id="install-3",
|
||||||
|
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:
|
||||||
|
create_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"},
|
||||||
|
)
|
||||||
|
prompt_package_id = create_response.json()["data"]["promptPackage"]["id"]
|
||||||
|
consume_response = await client.post(
|
||||||
|
f"/pmflow-os/projects/{project.id}/execution-handoffs/{prompt_package_id}/consume",
|
||||||
|
json={"runtime_target": "codex", "consumer_label": "Codex", "note": "开始开发"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert consume_response.status_code == 200
|
||||||
|
payload = consume_response.json()["data"]
|
||||||
|
assert payload["execution"]["status"] == "coding"
|
||||||
|
assert payload["recentProgressEvents"][0]["event_type"] == "handoff_consumed"
|
||||||
|
assert payload["recentProgressEvents"][0]["derived_task_status"] == "coding"
|
||||||
|
assert payload["recentProgressEvents"][0]["repo"]["full_name"] == "pmflow/handoff-consume"
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
await session.execute(select(PromptPackage).where(PromptPackage.id == int(prompt_package_id)))
|
||||||
|
).scalar_one()
|
||||||
|
latest_handoff = prompt.package_payload["latest_handoff"]
|
||||||
|
assert latest_handoff["handoff_state"] == "consumed"
|
||||||
|
assert latest_handoff["consumer_label"] == "Codex"
|
||||||
|
assert latest_handoff["runtime_target"] == "codex"
|
||||||
|
event_types = [
|
||||||
|
row.event_type
|
||||||
|
for row in (
|
||||||
|
await session.execute(select(ProgressEvent).where(ProgressEvent.task_id == task.id).order_by(ProgressEvent.id.asc()))
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
assert event_types == ["handoff_created", "handoff_consumed"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execution_handoff_rejects_task_outside_project(db_env) -> None:
|
async def test_execution_handoff_rejects_task_outside_project(db_env) -> None:
|
||||||
session = db_env
|
session = db_env
|
||||||
|
|
|
||||||
|
|
@ -807,6 +807,50 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
|
||||||
|
|
||||||
## 8. 进度台账
|
## 8. 进度台账
|
||||||
|
|
||||||
|
### 2026-04-26 / main-flow-p1-execution-handoff-consume / done
|
||||||
|
|
||||||
|
完成内容:
|
||||||
|
|
||||||
|
- 在 D.2 的 `handoff_created` 起点上继续补齐执行交接消费闭环,新增 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`。
|
||||||
|
- 后端校验项目成员权限、项目边界与 `PromptPackage(package_kind='execution_handoff')` 边界;消费动作只记录运行端领取,不执行本机命令、不写宿主机配置。
|
||||||
|
- `PMFlowOSProjectionService.consume_execution_handoff` 将 `PromptPackage.package_payload.latest_handoff` 更新为 `handoff_state=consumed`,并写入 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`。
|
||||||
|
- `execution-summary` 会因最新 `handoff_consumed` 事件把执行状态投影为 `coding`,前端执行桥可显示“运行端领取确认”。
|
||||||
|
- 前端 `ExecutionBridgePage` 在真实模式新增“标记为已领取”动作,调用 `consumePMFlowExecutionHandoff` 后 refetch `truth-state / execution-summary`;demo 模式仍只走本地演示推进。
|
||||||
|
- 修复 D.2 中 active `PromptPackage.package_payload` 嵌套 JSONB 更新可能未被 SQLAlchemy 标脏的问题,显式使用 `flag_modified`。
|
||||||
|
|
||||||
|
涉及文件:
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
- `CLAUDE.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` 通过:`10 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。
|
||||||
|
- `cd frontend && npm run test -- src/features/pmflow-os/store.api.test.tsx` 通过:`3 passed`。
|
||||||
|
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`4 passed`。
|
||||||
|
- `cd frontend && npm run typecheck` 通过。
|
||||||
|
- `cd frontend && npm run build` 通过。
|
||||||
|
|
||||||
|
未完成或风险:
|
||||||
|
|
||||||
|
- 本轮仍不执行真实命令;Codex / Local Bridge / GitHub agent 的 branch、commit、PR、test 回写仍属于下一步 D.3。
|
||||||
|
- 本轮新增后端运行时代码但未执行热部署/重启;当前环境是本地开发工作区,未连接实际部署服务。部署环境需要按实际栈执行 backend restart/reload。
|
||||||
|
|
||||||
|
下一步建议:
|
||||||
|
|
||||||
|
- 进入 D.3:补 Dev Sync 写回闭环,把运行端执行结果回写为 branch/commit/PR/test progress event,并投影回 `execution-summary / truth-state`。
|
||||||
|
|
||||||
### 2026-04-26 / big-module-integration-p1 / done
|
### 2026-04-26 / big-module-integration-p1 / done
|
||||||
|
|
||||||
完成内容:
|
完成内容:
|
||||||
|
|
|
||||||
|
|
@ -197,9 +197,9 @@
|
||||||
- `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/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/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/account-settings/AccountSettingsPanel.tsx` 是 `/me` 首版设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好。`frontend/src/api/accountSettings.ts` 是前端合同与本地持久化层;V1 UI 明确区分 `仅本机暂存 / 已保存到当前账号 / 保存失败`,当前实现只使用本机暂存,不假装云端账号保存;后续如接后端,应新增独立 `account_settings_api`,不能写入 PMFlow OS truth store 或 auth core。
|
||||||
- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=<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 成功。
|
- `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 create/consume 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。
|
||||||
- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、PR/测试与回写状态页;A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。
|
- `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,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding;当前仍不调用真实 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 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')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;两步都不直接执行本机命令;`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,7 +89,7 @@
|
||||||
- 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 与 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')` 起点,不直接执行本机命令
|
- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit 与 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')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;两步都不直接执行本机命令
|
||||||
- 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`
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
- 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 大模块并行公共壳:参考 `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`;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`
|
- 新增前端 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`、`consumePMFlowExecutionHandoff`、`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`
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
- 个人空间首页 / 空间层入口前端产品域:参考 `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/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`
|
||||||
- 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 必须明确显示 `仅本机暂存 / 保存失败`,不得假装云端账号保存
|
- 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 共享 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 create/consume 均调后端并 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`
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ export interface PMFlowExecutionHandoffRequest {
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PMFlowExecutionHandoffConsumeRequest {
|
||||||
|
runtime_target?: PMFlowExecutionRuntimeTarget | null;
|
||||||
|
consumer_label?: string | null;
|
||||||
|
note?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PMFlowPromptPackage {
|
export interface PMFlowPromptPackage {
|
||||||
id: string;
|
id: string;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
|
|
@ -177,6 +183,18 @@ export async function createPMFlowExecutionHandoff(
|
||||||
return unwrap(data);
|
return unwrap(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function consumePMFlowExecutionHandoff(
|
||||||
|
projectId: string,
|
||||||
|
promptPackageId: string,
|
||||||
|
payload: PMFlowExecutionHandoffConsumeRequest = {}
|
||||||
|
): Promise<PMFlowExecutionSummary> {
|
||||||
|
const { data } = await api.post<ApiEnvelope<PMFlowExecutionSummary>>(
|
||||||
|
`/pmflow-os/projects/${projectId}/execution-handoffs/${promptPackageId}/consume`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
return unwrap(data);
|
||||||
|
}
|
||||||
|
|
||||||
export async function createPMFlowThreadMessage(
|
export async function createPMFlowThreadMessage(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
threadId: string,
|
threadId: string,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export default function ExecutionBridgePage() {
|
||||||
executionSummaryError,
|
executionSummaryError,
|
||||||
refetchExecutionSummary,
|
refetchExecutionSummary,
|
||||||
createExecutionHandoff,
|
createExecutionHandoff,
|
||||||
|
consumeExecutionHandoff,
|
||||||
advanceExecution,
|
advanceExecution,
|
||||||
} = usePMFlowOsStore();
|
} = usePMFlowOsStore();
|
||||||
const demoSummary = dataSourceMode === "demo" ? createDemoExecutionSummary(state) : null;
|
const demoSummary = dataSourceMode === "demo" ? createDemoExecutionSummary(state) : null;
|
||||||
|
|
@ -107,6 +108,7 @@ export default function ExecutionBridgePage() {
|
||||||
<ContextPackagePanel
|
<ContextPackagePanel
|
||||||
summary={summary}
|
summary={summary}
|
||||||
onCreateHandoff={isDemo ? undefined : createExecutionHandoff}
|
onCreateHandoff={isDemo ? undefined : createExecutionHandoff}
|
||||||
|
onConsumeHandoff={isDemo ? undefined : consumeExecutionHandoff}
|
||||||
onDemoPush={isDemo ? advanceExecution : undefined}
|
onDemoPush={isDemo ? advanceExecution : undefined}
|
||||||
/>
|
/>
|
||||||
<ExecutionReviewPanel summary={summary} />
|
<ExecutionReviewPanel summary={summary} />
|
||||||
|
|
@ -141,6 +143,7 @@ function ExecutionStatusStrip({ summary }: { summary: PMFlowExecutionSummary })
|
||||||
function ContextPackagePanel({
|
function ContextPackagePanel({
|
||||||
summary,
|
summary,
|
||||||
onCreateHandoff,
|
onCreateHandoff,
|
||||||
|
onConsumeHandoff,
|
||||||
onDemoPush,
|
onDemoPush,
|
||||||
}: {
|
}: {
|
||||||
summary: PMFlowExecutionSummary;
|
summary: PMFlowExecutionSummary;
|
||||||
|
|
@ -150,6 +153,7 @@ function ContextPackagePanel({
|
||||||
runtime_target: PMFlowExecutionRuntimeTarget;
|
runtime_target: PMFlowExecutionRuntimeTarget;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
onConsumeHandoff?: (promptPackageId: string) => void;
|
||||||
onDemoPush?: () => void;
|
onDemoPush?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const prompt = summary.promptPackage;
|
const prompt = summary.promptPackage;
|
||||||
|
|
@ -241,6 +245,28 @@ function ContextPackagePanel({
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{prompt && onConsumeHandoff ? (
|
||||||
|
<section className="rounded-xl border border-[#e8eef8] bg-white p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Terminal className="mt-0.5 h-4 w-4 shrink-0 text-slate-500" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-sm font-semibold text-slate-900">运行端领取确认</div>
|
||||||
|
<p className="mt-1 text-xs leading-5 text-slate-500">
|
||||||
|
只记录交接包已被 Codex/Local Bridge 等运行端领取,不从浏览器或后端执行本机命令。
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-4 inline-flex w-full items-center justify-center gap-2 rounded-lg border border-[#dfe7f3] bg-white px-5 py-2 text-sm font-semibold text-slate-700 transition hover:border-[#2463EB] hover:text-[#2463EB]"
|
||||||
|
onClick={() => onConsumeHandoff(prompt.id)}
|
||||||
|
>
|
||||||
|
<Terminal className="h-4 w-4" />
|
||||||
|
标记为已领取
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{onDemoPush ? (
|
{onDemoPush ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
acceptPMFlowChangeItem,
|
acceptPMFlowChangeItem,
|
||||||
commitPMFlowChangeSet,
|
commitPMFlowChangeSet,
|
||||||
|
consumePMFlowExecutionHandoff,
|
||||||
createPMFlowExecutionHandoff,
|
createPMFlowExecutionHandoff,
|
||||||
createPMFlowThreadMessage,
|
createPMFlowThreadMessage,
|
||||||
deferPMFlowChangeItem,
|
deferPMFlowChangeItem,
|
||||||
|
|
@ -25,6 +26,7 @@ vi.mock("@/api/pmflowOs", () => ({
|
||||||
getPMFlowChangeSet: vi.fn(),
|
getPMFlowChangeSet: vi.fn(),
|
||||||
acceptPMFlowChangeItem: vi.fn(),
|
acceptPMFlowChangeItem: vi.fn(),
|
||||||
commitPMFlowChangeSet: vi.fn(),
|
commitPMFlowChangeSet: vi.fn(),
|
||||||
|
consumePMFlowExecutionHandoff: vi.fn(),
|
||||||
createPMFlowExecutionHandoff: vi.fn(),
|
createPMFlowExecutionHandoff: vi.fn(),
|
||||||
editPMFlowChangeItem: vi.fn(),
|
editPMFlowChangeItem: vi.fn(),
|
||||||
deferPMFlowChangeItem: vi.fn(),
|
deferPMFlowChangeItem: vi.fn(),
|
||||||
|
|
@ -34,6 +36,7 @@ const getTruthStateMock = vi.mocked(getPMFlowTruthState);
|
||||||
const getExecutionSummaryMock = vi.mocked(getPMFlowExecutionSummary);
|
const getExecutionSummaryMock = vi.mocked(getPMFlowExecutionSummary);
|
||||||
const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage);
|
const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage);
|
||||||
const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
|
const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
|
||||||
|
const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff);
|
||||||
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);
|
||||||
|
|
@ -224,6 +227,44 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
recentDeliveries: [],
|
recentDeliveries: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
});
|
});
|
||||||
|
consumeExecutionHandoffMock.mockResolvedValue({
|
||||||
|
projectId: "123",
|
||||||
|
execution: {
|
||||||
|
taskId: "task-99",
|
||||||
|
status: "coding",
|
||||||
|
prompt: "真实 prompt package",
|
||||||
|
commitLog: ["执行交接已领取"],
|
||||||
|
prStatus: "pending",
|
||||||
|
testSummary: "pending",
|
||||||
|
writebackStatus: "coding",
|
||||||
|
},
|
||||||
|
devSync: { active_task_count: 1, recent_event_count: 2 },
|
||||||
|
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: { latest_handoff: { handoff_state: "consumed" } },
|
||||||
|
created_at: "2026-04-26T00:00:00",
|
||||||
|
},
|
||||||
|
repoBindings: [],
|
||||||
|
activeDevTasks: [],
|
||||||
|
recentProgressEvents: [
|
||||||
|
{
|
||||||
|
id: "702",
|
||||||
|
event_type: "handoff_consumed",
|
||||||
|
source: "pmflow_os",
|
||||||
|
summary: "执行交接已领取",
|
||||||
|
workflow_state: "coding",
|
||||||
|
derived_task_status: "coding",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
recentDeliveries: [],
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -273,6 +314,15 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
act(() => result.current.consumeExecutionHandoff("501"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(consumeExecutionHandoffMock).toHaveBeenCalledWith(
|
||||||
|
"123",
|
||||||
|
"501",
|
||||||
|
expect.objectContaining({ runtime_target: "codex", consumer_label: "Codex" })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.current.state.requirements).toEqual([]);
|
expect(result.current.state.requirements).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -296,6 +346,7 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
|
|
||||||
expect(acceptItemMock).not.toHaveBeenCalled();
|
expect(acceptItemMock).not.toHaveBeenCalled();
|
||||||
expect(createExecutionHandoffMock).not.toHaveBeenCalled();
|
expect(createExecutionHandoffMock).not.toHaveBeenCalled();
|
||||||
|
expect(consumeExecutionHandoffMock).not.toHaveBeenCalled();
|
||||||
expect(getExecutionSummaryMock).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,6 +3,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
acceptPMFlowChangeItem,
|
acceptPMFlowChangeItem,
|
||||||
commitPMFlowChangeSet,
|
commitPMFlowChangeSet,
|
||||||
|
consumePMFlowExecutionHandoff,
|
||||||
createPMFlowExecutionHandoff,
|
createPMFlowExecutionHandoff,
|
||||||
createPMFlowExtraction,
|
createPMFlowExtraction,
|
||||||
createPMFlowThreadMessage,
|
createPMFlowThreadMessage,
|
||||||
|
|
@ -532,6 +533,7 @@ interface PMFlowOsStoreValue {
|
||||||
promoteAcceptedToGraph: (changeSetId?: string | null) => void;
|
promoteAcceptedToGraph: (changeSetId?: string | null) => void;
|
||||||
pushTaskToExecution: (taskId: string) => void;
|
pushTaskToExecution: (taskId: string) => void;
|
||||||
createExecutionHandoff: (payload: PMFlowExecutionHandoffRequest) => void;
|
createExecutionHandoff: (payload: PMFlowExecutionHandoffRequest) => void;
|
||||||
|
consumeExecutionHandoff: (promptPackageId: string) => void;
|
||||||
advanceExecution: () => void;
|
advanceExecution: () => void;
|
||||||
writeBackExecution: (outcome: "passed" | "failed") => void;
|
writeBackExecution: (outcome: "passed" | "failed") => void;
|
||||||
publishRelease: () => void;
|
publishRelease: () => void;
|
||||||
|
|
@ -742,6 +744,19 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
mutateDemoOnly((current) => pushTaskToExecutionMutation(current, payload.task_id));
|
mutateDemoOnly((current) => pushTaskToExecutionMutation(current, payload.task_id));
|
||||||
},
|
},
|
||||||
|
consumeExecutionHandoff: (promptPackageId) => {
|
||||||
|
if (dataSource.mode === "api" && dataSource.projectId) {
|
||||||
|
runApiAction(() =>
|
||||||
|
consumePMFlowExecutionHandoff(dataSource.projectId || "", promptPackageId, {
|
||||||
|
runtime_target: "codex",
|
||||||
|
consumer_label: "Codex",
|
||||||
|
note: "PMFlow Execution Bridge 确认交接包已被运行端领取",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutateDemoOnly((current) => advanceExecutionMutation(current));
|
||||||
|
},
|
||||||
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