From f33073d1a2442e0394265d827c38bd70b2e4d55e Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 6 May 2026 00:55:22 +0800 Subject: [PATCH] feat(pmflow-os): add execution scope board --- docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md | 51 +- docs/architecture/ARCHITECTURE_INVENTORY.md | 2 +- docs/architecture/CODE_REFERENCE_INDEX.md | 4 +- docs/architecture/RUNTIME_GUARDRAILS.md | 2 +- .../pmflow-os/pages/ExecutionBridgePage.tsx | 505 ++++++++++++++++-- .../src/features/pmflow-os/store.api.test.tsx | 41 ++ 6 files changed, 565 insertions(+), 40 deletions(-) diff --git a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md index f6bfdeb9..9771c70d 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -766,14 +766,14 @@ commit gate 判定: 当前已完成: -**main-flow-d4.5-execution-run-workspace-redesign:开发执行 Run 工作台重构。** +**main-flow-d4.6-execution-scope-board:开发范围总览与需求执行队列。** 原因: -- `/execution-bridge` 已从模块堆叠重构为 `Execution Run Workspace`:顶部 Run Header、阶段 rail、左侧执行源包、中间当前阶段工作台、右侧 AI 决策面板、底部证据流水。 -- 执行页现在明确表达 `已确认需求 / 上下文与 Prompt 包 / 开发 / 测试 / 审查合并 / 回写` 的业务流转,所有主按钮由单点 Run 状态模型推导,避免散落按钮各自判断。 -- 手动回写从常驻表单收进 drawer;页面主界面优先展示阶段、源头、当前证据和下一步,不再像 dev-sync 调试录入台。 -- 本轮只改前端信息架构和测试,复用既有 `execution-summary / execution-capabilities / execution-progress-events`,不新增后端合同,不执行命令,不自动验收、合并或写 Project Memory。 +- `/execution-bridge` 已补齐开发范围层:首屏先展示“本轮开发范围”和“需求执行队列”,再进入选中任务的 Run 工作台。 +- 页面现在按 Requirement 聚合正式 Task;同一需求下多个开发任务可在左侧切换,没有任务的已确认需求显示“尚未形成开发任务”,无法关联需求的任务进入“未归属任务”,不反推关系。 +- Run Header 现在同时展示范围规模与选中 Run 状态,右侧 AI 副驾增加范围缺口提示;主按钮仍由选中 Run 的单点状态模型推导。 +- 本轮继续只改前端信息架构和测试,复用既有 `truth-state / execution-summary / execution-capabilities / execution-progress-events`,不新增后端合同,不执行命令,不自动验收、合并或写 Project Memory。 ### 7.1 下一步推荐 Packet:`main-flow-d5-execution-acceptance-loop` @@ -808,6 +808,47 @@ commit gate 判定: ## 8. 进度台账 +### 2026-05-06 / main-flow-d4.6-execution-scope-board / implemented + +完成内容: + +- 在 `/execution-bridge` 增加“本轮开发范围”总览,展示已确认需求、正式任务、执行中、待生成包、待测试/审查与阻塞数量。 +- 增加“需求执行队列”,按 Requirement 聚合开发任务;同一需求下多个 Task 可以切换到对应 Run 详情。 +- 对无法从 `state.requirements/state.tasks` 关联到需求的执行任务,进入“未归属任务”区,明确展示但不反推业务关系。 +- 保留 D4.5 的单 Run 工作台作为选中详情:中间展示当前任务的阶段 rail 与工作台,右侧展示执行源包、AI 阶段决策与范围缺口。 +- 新增前端只读范围模型,合并 `truth-state` 的 Requirement/Task 与 `execution-summary.activeDevTasks`,再为选中任务生成 scoped execution summary;不新增后端 API。 +- 焦点测试覆盖多任务开发范围、需求队列、任务切换和切换后的 Run 主动作。 + +涉及文件: + +- `frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx` +- `frontend/src/features/pmflow-os/store.api.test.tsx` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/architecture/RUNTIME_GUARDRAILS.md` + +验证: + +- `cd frontend && npx tsc --noEmit --pretty false --incremental false` 通过。 +- `cd frontend && npm run test -- src/features/pmflow-os` 通过:35 passed。 +- `cd frontend && npm run build` 通过。 +- Playwright 浏览器检查 `http://localhost:5178/execution-bridge?demo=1` 通过:桌面与移动端均无页面横向溢出,首屏可见“本轮开发范围 / 需求执行队列”,点击“前置企业接入权限说明”后能切换到对应 Run;截图落点: + - `output/playwright/execution-scope-board-d46-desktop.png` + - `output/playwright/execution-scope-board-d46-desktop-selected.png` + - `output/playwright/execution-scope-board-d46-mobile.png` + - `output/playwright/execution-scope-board-d46-mobile-selected.png` +- 浏览器控制台仍有既有 `GET /api/ai-readiness/project-launch` 401,属于现有 AI readiness 探针鉴权噪声,本 Packet 未改该链路。 + +未完成或风险: + +- Requirement -> Task -> Execution Run 关联当前仍优先消费现有前端读模型和执行摘要;后续若后端提供稳定 run/list 合同,应将 scoped summary 推导下沉到后端。 +- 本轮不新增真实验收、技术审查、合并或 Project Memory 回写状态合同,这仍属于 `main-flow-d5-execution-acceptance-loop`。 + +下一步建议: + +- 进入 `main-flow-d5-execution-acceptance-loop`:在 D4.6 的范围视图基础上,补真实验收、技术审查、合并确认和项目记忆回写状态,供 Space next action 与 Release/Growth 门禁稳定消费。 + ### 2026-05-06 / main-flow-d4.5-execution-run-workspace-redesign / implemented 完成内容: diff --git a/docs/architecture/ARCHITECTURE_INVENTORY.md b/docs/architecture/ARCHITECTURE_INVENTORY.md index 20d49fed..24889935 100644 --- a/docs/architecture/ARCHITECTURE_INVENTORY.md +++ b/docs/architecture/ARCHITECTURE_INVENTORY.md @@ -213,7 +213,7 @@ - `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 `sendMessage` 会写 thread message 并触发后端真实 AI extraction;显式 `proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest、单条 iteration recommendation -> ChangeSet draft 与同组 iteration recommendation 批量 -> ChangeSet draft 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功;AI extraction 失败显示“结构化失败/待补充”,不创建模板 ChangeSet。 - `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` 进入时通过 `origin=change_review / returnTo=change_review / changeSetId / itemId` 保留审查上下文,主动作返回待确认队列,补充证据才回指挥台;已 committed 的轮次显示为结构化写入快照并转向项目记忆或新变更链路。`ChangeReviewPage.tsx` 是决策与待确认中心,真实模式下顶部识别统计、待确认列表与右侧 AI 副驾评估都只消费后端 `changeSets / changeItems / evidence`,没有真实待审项时显示空态,禁止用 demo 审查项补位;确认写入 Project Memory 前,`pending_review / accepted / edited / deferred` ChangeItem 均可继续 accept/edit/defer,只有 `committed / skipped` 或关闭轮次不可再操作;`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate`、`latestValidationRun` 与 `releaseSignals`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作、发布验证记录和发布后 feedback/revenue/ROI/compliance/recommendation 信号,缺少发布后信号时显示 empty/warning,不执行发布命令、不展示 mock 发布结果;页面可展示 recommendation quality grouping,把真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft,生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不创建正式任务;`ChangeReviewPage.tsx` 会展示 `iteration_recommendation_group` evidence 的来源组、feedback 数、recommendation 数、风险、影响和审查优先级;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;静态发布/增长原型只保留在显式 demo 模式。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,Release Growth P1 已接入 execution evidence gate,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照,Release Feedback P1 已接入 `POST /release-feedback-ingests` 写入 `FeedbackIngest` 并生成 `IterationRecommendation`,Iteration Loop P1 已接入 `POST /iteration-recommendations/{recommendation_id}/change-set-drafts` 把 recommendation 回流到 Change Review,Iteration Loop P2 已接入 `POST /iteration-recommendation-groups/{group_key}/change-set-drafts` 并在 `releaseSignals.recommendationGroups` 暴露分组/去重/风险/影响合同;真实 AI extraction 已接入独立 `change_review` protected scene、`pmflow_change_review_extraction_v1` 运行合同、`PROMPT_PMFLOW_CHANGE_REVIEW_EXTRACTION` Prompt 资产与后端 `llm_service.generate_structured_object`,成功时生成 open ChangeSet、pending_review ChangeItems 与 thread message evidence,失败时 fail-closed,不把 mock 当真实失败 fallback,也不复用 Project Launch draft prompt/function。`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` 表达“未确认不得写入”的局部提示。 - Execution Result Protocol P1 将 Execution Bridge 的执行结果回写收敛为 `execution_result_protocol_v1`:`backend/app/schemas/pmflow_os.py` 接收 `execution_status` 与 `blocker_summary`,`backend/app/services/pmflow_os_projection_service.py` 写入 `ProgressEvent.payload.protocol_version` 与 `execution_result` envelope,同时保留 `branch_name / commit_sha / pr_number / test_summary / workflow_state` flat 字段兼容现有投影;`frontend/src/api/pmflowOs.ts` 和 `ExecutionBridgePage.tsx` 支持手动记录 status/blocker。该链路只记录 Codex/IDE/Local Bridge/GitHub agent 已产生的结果,不执行本机命令、不推送、不发布、不自动创建正式任务;`execution-summary`、`releaseGate` 与 Team Feed 均从 `ProgressEvent` 只读消费。 -- Main Flow D4/D4.5 已把 Execution Capability Graph 接入并重构 `/execution-bridge` 为开发执行 Run 工作台:`frontend/src/features/pmflow-os/store.tsx` 读取并刷新真实 `GET /pmflow-os/projects/{project_id}/execution-capabilities`,`ExecutionBridgePage.tsx` 通过单点 Run 状态模型推导 `已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段、当前主动作、证据数和阻塞原因;页面结构为 Run Header、阶段 rail、执行源包、当前阶段工作台、AI 阶段决策面板和底部证据流水。handoff 主按钮受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,技术审查责任缺失时 store 会阻止真实 handoff,缺 repo 时只允许表达交接草案;手动回写在 drawer 中记录已发生的 branch/commit/PR/test/blocker/status。该层仍只做能力判定、交接合同和结果回写展示,不执行本机命令、不触发真实 agent、不自动验收、不自动合并、不自动发布。 +- Main Flow D4/D4.5/D4.6 已把 Execution Capability Graph 接入并重构 `/execution-bridge` 为开发范围总览 + 选中 Run 工作台:`frontend/src/features/pmflow-os/store.tsx` 读取并刷新真实 `GET /pmflow-os/projects/{project_id}/execution-capabilities`,`ExecutionBridgePage.tsx` 先消费 `truth-state.requirements/tasks` 与 `execution-summary.activeDevTasks` 形成前端只读开发范围模型,展示本轮需求/任务总览、按 Requirement 聚合的需求执行队列、未归属任务和选中任务 Run 详情;选中 Run 继续通过单点状态模型推导 `已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段、当前主动作、证据数和阻塞原因。handoff 主按钮受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,技术审查责任缺失时 store 会阻止真实 handoff,缺 repo 时只允许表达交接草案;手动回写在 drawer 中记录已发生的 branch/commit/PR/test/blocker/status。该层仍只做能力判定、交接合同和结果回写展示,不执行本机命令、不触发真实 agent、不自动验收、不自动合并、不自动发布。 - Project Knowledge Hub 读模型由 `GET /pmflow-os/projects/{project_id}/project-memory/overview`、`items`、`items/{item_id}`、`assets`、`context-packs`、`writebacks` 承载,后端在 `backend/app/services/pmflow_os_projection_service.py` 从 `truth_nodes / tasks / semantic_facts / pmflow_evidence_links / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items` 聚合首页雷达、全部记忆、项目资产、上下文包和写入版本记录。前端对应 `ProjectMemoryPage.tsx`、`ProjectMemoryCollectionPages.tsx` 与 `projectMemoryUtils.ts`:列表时间使用短格式,抽屉显示完整时间和北京时间;首页卡片与二级列表单条点击打开右侧详情抽屉,展示来源证据、影响对象和 AI 执行动作跳转;全部记忆支持类型、阶段、来源、状态、搜索筛选。该读模型只读,资产可自动索引,进入项目真相层仍必须经过 Change Review 或可信规则确认。 - `frontend/src/features/account-settings/AccountSettingsPanel.tsx` 是 `/me` 设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,并在通知偏好区展示页面内提醒消费说明、提醒强度和证据展示配置。`frontend/src/api/accountSettings.ts` 是前端合同层;有 token 时优先调用 `backend/app/api/account_settings_api/`,失败时回落浏览器本机暂存;P2 新增 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,将四类通知偏好映射为本地 UI 的待确认、Execution Bridge、Release/Growth 和失败提醒显示/静默状态;P3 新增 `getReleaseGateNotificationPreference(settings, event)`,把 Release/Growth 的 `release_gate_* / release_validation_*` 事件映射到 `releaseGate` 偏好,把 `failed_event` 映射到 `failureAlert` 偏好;P5/P6 的 `getPostReleaseNotificationPreference(settings, event)` 把发布后 `feedback_received / revenue_signal / compliance_blocked` 映射到 `releaseGate` 偏好,把 `iteration_recommendation / next_change_review` 映射到 `pendingConfirmation` 偏好和 `change_review` surface,并返回 `spaceReadable / evidencePolicy / reminderPolicy` 供 Main Flow 与 Space 消费;P7 将通知偏好扩展为 `reminderIntensity=silent|normal|prominent` 与 `evidenceDisplay=compact|expanded`,并新增 `getAccountReminderConsumption(settings, event)` 覆盖 `change_review_high_risk / iteration_draft_ready / release_blocked / execution_stalled`,输出 `notificationTransport=in_app_only / evidencePolicy=preserve_business_evidence / auditPolicy=preserve_audit_state`;P8 新增 `getReviewDecisionNotificationPreference(settings, event)` 覆盖 `review_requested / review_approved / review_changes_requested / review_rejected / high_risk_approval_required`,继续使用 `pendingConfirmation` 偏好和 `change_review` surface,并声明 reviewerRouting、approval、evidence、audit state 均必须保留。该合同只控制页面内提醒表达,不代表邮件、桌面推送或系统通知中心已接入;偏好关闭也不得隐藏业务证据、reviewerRouting、approval、recommendation、ChangeSet、执行状态或审计状态。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。 - `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 `sendMessage` 会写 thread message 并触发后端真实 AI extraction;显式 `proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、AI feedback、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest 与 iteration recommendation -> ChangeSet draft 均调用 `/pmflow-os` 后端并 refetch;AI feedback 写入 `pmflow_ai_feedback_events`,在 `truth-state.aiFeedback` 恢复当前用户选择,并由后端按 `prompt_key` 生成待复核 `JudgeRun / OptimizerCandidate` 质量优化候选,用户改回赞同时挂起此前候选,Prompt Asset quality summary 会聚合 PMFlow 反馈与优化候选;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功;AI extraction 失败显示“结构化失败/待补充”,不创建模板 ChangeSet。 diff --git a/docs/architecture/CODE_REFERENCE_INDEX.md b/docs/architecture/CODE_REFERENCE_INDEX.md index 3db45014..785a5335 100644 --- a/docs/architecture/CODE_REFERENCE_INDEX.md +++ b/docs/architecture/CODE_REFERENCE_INDEX.md @@ -126,9 +126,9 @@ - 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;登录后路由入口为 `/space`,未登录由 `frontend/src/auth/RouteBoundary.tsx` 跳转 `/login?redirect=/space`;首屏以 `nextActionQueue` 展示“我的下一步”,并保留执行准备度、继续推进、待我确认、发布关注、发布后反馈关注、下一轮迭代草稿关注、执行中 / AI 正在推进、暂存想法、草案箱、缺项目归属补全与最近项目明细;红框三块在首页只作为工作雷达预览,暂存想法、草案箱、最近项目均最多露出少量高优先级条目,完整处理分别进入 `/space/ideas`、`/space/drafts` 与 `/space/projects?view=recent`。`frontend/src/features/personal-space/SpaceCollectionPages.tsx` 承载二级页:`/space/ideas` 支持暂存想法筛选、搜索、补充、归档与转 Project Launch 草案,`/space/drafts` 支持待确认草案筛选、搜索与回主流程确认;这些页面仍使用空间壳,不直接写项目真相层。Space 同时展示 AI readiness 客户视图卡片,管理员可从侧边栏或顶部状态进入 `/ai-config?tab=setup`。空间级 AI 管家在 `frontend/src/features/personal-space/components/SpaceAIConcierge.tsx`,入口只挂 `/space`,向现有 `/rag/ask` 注入个人空间上下文包并复用 QA session 历史能力;它只能围绕个人空间、跨项目下一步、待确认、执行中、草案、暂存想法和 AI 能力状态工作,真实写操作必须回主流程确认或跳转,不得声称已执行。全局 `Layout` 不再挂载旧 `FloatingQAChat` 悬浮球,普通后台页不显示无上下文聊天球,项目页继续使用项目级 `AI 副驾`。Space 左侧导航只承载稳定空间目的地,当前保留“空间首页”“暂存想法”“草案箱”“所有项目”和管理员“AI 使用设置”。点击项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,review attention 待审项进入 `/change-review?projectId=&changeSetId=`,reviewer 缺失 warning 进入已有 `/team-layer?projectId=`,执行中与执行准备度项进入 `/execution-bridge?projectId=`,发布关注和发布后反馈关注项进入 `/release-growth?projectId=`;执行失败和回写失败项进入 `/execution-bridge?projectId=`;Space 聚合项目摘要、PMFlow OS `truth-state.reviewerRouting / truth-state.releases / execution-summary.releaseGate / execution-summary.releaseSignals / execution-summary.recentProgressEvents / execution-summary.recentDeliveries / execution-capabilities` 和真实 `GET /space/saved-ideas`,并兼容读取旧 AI PMO `waiting_manual / draft_pending_confirm` 工作项;`missing_project_scope` 仅在 Space 选择项目后复用 `POST /ai-pmo/work-items/{id}/decision`,不新增旁路接口,不直接写项目真相层。缺数据时显示明确空态或 warning,不虚构 reviewer、审查意见、待审 ChangeSet、执行能力、工具连接、验证、反馈、收入、ROI、合规、iteration recommendation 或 ChangeSet draft;暂存想法是唯一允许 Space 写入的 pre-project seed,保存原始输入、来源、状态、准备度和 draft 关联,生成草案只进入 Project Launch human review;除此之外不直接写 Project Memory、ChangeSet 状态、PMFlowApproval、execution event、ReleaseValidationRun、FeedbackIngest、RevenueEvent 或 ComplianceCheckRun;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该空间 Chrome - 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式登录后路由入口为 `/project-launch`,默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 taskized draft job/confirm API;AI 草案创建链路是 `POST /pmflow-launch/draft-jobs -> GET /pmflow-launch/draft-jobs/{job_id} -> POST /pmflow-launch/drafts/{draft_id}/confirm`,补充一句走 `POST /pmflow-launch/drafts/{draft_id}/revise` 或创建新的 `merge_supplement` job,旧 `POST /ai-steward/project-from-text` 只返回 410 迁移错误;只有显式 `?demo=1` 或 `?dataSource=demo` 才允许匿名并启用 `mockData.ts` 本地演示状态机,真实模式的操作流、生成过程、审查建议、附件引用和计数从当前输入、job 状态与后端返回草案派生,不再使用周报 demo 文案兜底;真实模式下 AI 未就绪时先渲染 `AISetupRequiredPanel`,按权限展示记录想法、查看空间状态或平台 AI 能力处理 CTA,不提供空项目创建入口,避免在缺少结构化上下文时提前创建项目壳。401/无 token 通过 `frontend/src/lib/apiError.ts` 归一为 `auth_required/session_expired`,通过 `frontend/src/lib/pendingIntent.ts` 保留输入;`input_needs_more_info` 422 归一为 `input_invalid` 并停在补充输入失败态,用户可见失败态不得展示 `Request failed with status code 401` 或把 401 归为网络异常 - 前端身份边界与产品化错误:参考 `frontend/src/auth/RouteBoundary.tsx`、`frontend/src/lib/rbac.ts`、`frontend/src/lib/apiError.ts`、`frontend/src/lib/pendingIntent.ts`;`/space`、真实 `/project-launch`、`/me`、主链 standalone、空间管理入口与平台运营入口均在前端边界收口。空间管理入口使用 workspace role,`/platform/*` 和平台治理入口使用 `platform_context.capabilities`,后端 JWT/RBAC 仍是最终校验;pending intent 只能恢复 project launch 输入或承接已经生成好的 Project Launch draft,不允许登录后自动重放真实写请求 -- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=` 走 React Query 读取真实 `truth-state`、`execution-summary` 与 `execution-capabilities`,真实模式下 `sendMessage` 会先写 thread message,再调用 `createPMFlowExtraction` 触发后端真实 AI extraction,成功后 refetch 出 open ChangeSet / pending_review ChangeItems;显式 `proposed_items` extraction 仍保留给受控 draft 场景;ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、AI feedback、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest、单条 iteration recommendation -> ChangeSet draft 与同组 iteration recommendation 批量 -> ChangeSet draft 均调后端并 refetch,不本地 promote 入 Project Memory、不伪造 approval、execution event、发布验证记录、发布后信号或下一轮 draft;D4 后 `createExecutionHandoff` 除正式 Task gate 外会读取 `execution-capabilities`,在技术审查责任缺失时阻止 handoff,避免把 agent 执行绕过 reviewer gate;D4.5 后 `/execution-bridge` 的主按钮由页面级 Run 状态模型消费 store 数据统一推导,不再散落在上下文包、回写视图和 AI 副驾卡片里;AI feedback 通过 `createPMFlowAIFeedback` 写入 `pmflow_ai_feedback_events` 并在 `truth-state.aiFeedback` 恢复当前用户选择;AI extraction 失败在 CommandCenter / StructuredExtraction 显示“结构化失败/待补充”,不展示模板假候选;`?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=` 走 React Query 读取真实 `truth-state`、`execution-summary` 与 `execution-capabilities`,真实模式下 `sendMessage` 会先写 thread message,再调用 `createPMFlowExtraction` 触发后端真实 AI extraction,成功后 refetch 出 open ChangeSet / pending_review ChangeItems;显式 `proposed_items` extraction 仍保留给受控 draft 场景;ChangeItem `accept/edit/defer`、ChangeSet commit、review decision、AI feedback、Execution Handoff create/consume、execution progress writeback、release validation run create、release feedback ingest、单条 iteration recommendation -> ChangeSet draft 与同组 iteration recommendation 批量 -> ChangeSet draft 均调后端并 refetch,不本地 promote 入 Project Memory、不伪造 approval、execution event、发布验证记录、发布后信号或下一轮 draft;D4 后 `createExecutionHandoff` 除正式 Task gate 外会读取 `execution-capabilities`,在技术审查责任缺失时阻止 handoff,避免把 agent 执行绕过 reviewer gate;D4.5 后 `/execution-bridge` 的主按钮由页面级 Run 状态模型消费 store 数据统一推导,不再散落在上下文包、回写视图和 AI 副驾卡片里;D4.6 后 `/execution-bridge` 额外消费 `truth-state.requirements/tasks` 与 `execution-summary.activeDevTasks` 形成前端只读开发范围模型,按 Requirement 聚合任务、展示未归属任务并支持切换选中 Run;AI feedback 通过 `createPMFlowAIFeedback` 写入 `pmflow_ai_feedback_events` 并在 `truth-state.aiFeedback` 恢复当前用户选择;AI extraction 失败在 CommandCenter / StructuredExtraction 显示“结构化失败/待补充”,不展示模板假候选;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success` - Account Settings / `/me` 前端产品域与 P1-P8 偏好合同:参考 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`、`frontend/src/features/pmflow-os/pages/AccountPage.tsx`、`frontend/src/api/accountSettings.ts`、`backend/app/api/account_settings_api/`、`backend/app/schemas/account_settings.py` 与 `backend/app/services/account_settings_service.py`;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,移动端使用分组列表与 bottom sheet,账户退出仍复用 `AccountControls.tsx`,不改 OAuth/JWT/auth core;P1 后端合同提供 `GET /account-settings/me` 与 `PUT /account-settings/me`,已由 `backend/app/api/router.py` 挂入核心路由组,按当前 `tenant_id + user_id` 保存账号偏好,前端有 token 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`;P2 在 `frontend/src/api/accountSettings.ts` 提供 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,把通知偏好映射为页面内提醒消费模型;P3 新增 `getReleaseGateNotificationPreference(settings, event)` 供 Release/Growth 后续消费:`release_gate_*` 与 `release_validation_*` 使用 `releaseGate` 偏好,`failed_event` 使用 `failureAlert` 偏好;P5/P6 的 `getPostReleaseNotificationPreference(settings, event)` 供发布后反馈与下一轮待确认消费:`feedback_received / revenue_signal / compliance_blocked` 使用 `releaseGate` 偏好,`iteration_recommendation / next_change_review` 使用 `pendingConfirmation` 偏好并落到 `change_review` surface;P7 将每类通知偏好扩展为 `reminderIntensity=silent|normal|prominent` 与 `evidenceDisplay=compact|expanded`,并新增 `getAccountReminderConsumption(settings, event)` 覆盖 `change_review_high_risk / iteration_draft_ready / release_blocked / execution_stalled`,稳定返回 `notificationTransport=in_app_only / evidencePolicy=preserve_business_evidence / auditPolicy=preserve_audit_state`;P8 新增 `getReviewDecisionNotificationPreference(settings, event)` 覆盖 `review_requested / review_approved / review_changes_requested / review_rejected / high_risk_approval_required`,统一使用 `pendingConfirmation` 偏好和 `change_review` surface,返回 `reviewerRoutingPolicy=preserve_reviewer_routing / approvalPolicy=preserve_approval_state`,偏好关闭只静默页面内提醒,不隐藏 reviewerRouting、approval、evidence 或 audit state;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态、validation run、releaseSignals 或下一轮 ChangeSet draft;这些 helper 只控制本地 UI 提醒消费合同,不代表真实邮件、桌面推送或系统通知中心已接入 -- 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`;`StructuredExtractionPage.tsx` 根据 `origin=change_review / returnTo=change_review / changeSetId / itemId` 区分指挥台读取检查、待确认证据检查和已写入快照,从待确认进入时主动作必须返回 `/change-review` 原轮次,不把用户强制带回指挥台;`ChangeReviewPage.tsx` 真实模式可对当前变更集请求审查、通过、要求修改或拒绝,展示审查状态、建议审查人、审查意见和高风险审批门禁,但不自动 commit,且在确认写入 Project Memory 前允许 `pending_review / accepted / edited / deferred` ChangeItem 继续 accept/edit/defer;`ExecutionBridgePage.tsx` 真实模式读取 `execution-summary` 与 `execution-capabilities`,D4.5 后页面形态为开发执行 Run 工作台:Run Header、`已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段 rail、左侧执行源包、中间当前阶段工作台、右侧 AI 阶段决策面板和底部证据流水;主动作由单点 Run 状态模型推导,必须受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,缺 repo 只能生成交接草案,技术审查责任缺失时不能发起 handoff;手动回写在 drawer 内记录已发生的 branch/commit/PR/test/blocker/status,不执行命令;执行结果只汇总运行端、PR/commit、测试、阻塞和下一步,不把测试通过自动视为验收完成;`TeamLayerPage.tsx` 真实模式读取 `state.reviewerRouting`、`state.feed` 与 `execution-capabilities`,以 `团队动态 / 决策台 / 角色收件箱 / 智能体工位 / AI 副驾` 展示团队协作态势、待审变更责任路由、执行能力与容量信号、个人执行准备度和工具准备度,只给责任建议、阻塞解释与跳转,不自动分派、不自动审查、不写 ChangeSet 或 Project Memory;决策台与待审路由卡片不得预留独立空白操作列,正文应使用完整卡片宽度,1-2 条路由按双列或单列密度展示;`ReleaseGrowthPage.tsx` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run、摄取 release feedback,并展示真实 feedback、revenue、ROI、compliance 与 recommendation 风险态,从真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft;生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不执行部署命令、不接外部支付、不接真实合规平台;所有真实证据和审计状态仍可见,静态高保真发布/增长数据仅保留在显式 demo 模式 +- 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`;`StructuredExtractionPage.tsx` 根据 `origin=change_review / returnTo=change_review / changeSetId / itemId` 区分指挥台读取检查、待确认证据检查和已写入快照,从待确认进入时主动作必须返回 `/change-review` 原轮次,不把用户强制带回指挥台;`ChangeReviewPage.tsx` 真实模式可对当前变更集请求审查、通过、要求修改或拒绝,展示审查状态、建议审查人、审查意见和高风险审批门禁,但不自动 commit,且在确认写入 Project Memory 前允许 `pending_review / accepted / edited / deferred` ChangeItem 继续 accept/edit/defer;`ExecutionBridgePage.tsx` 真实模式读取 `execution-summary` 与 `execution-capabilities`,D4.6 后页面形态为开发范围总览 + 需求执行队列 + 选中 Run 工作台:首屏展示本轮已确认需求、正式任务、执行中、待生成包、待测试/审查与阻塞数量,左侧按 Requirement 聚合任务、列出未归属任务并允许切换选中任务;选中任务详情继续展示 Run Header、`已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段 rail、当前阶段工作台、执行源包、AI 阶段决策面板和底部证据流水;主动作由单点 Run 状态模型推导,必须受正式 Task、待确认范围、repo/prompt package 与 reviewer gate 约束,缺 repo 只能生成交接草案,技术审查责任缺失时不能发起 handoff;手动回写在 drawer 内记录已发生的 branch/commit/PR/test/blocker/status,不执行命令;执行结果只汇总运行端、PR/commit、测试、阻塞和下一步,不把测试通过自动视为验收完成;`TeamLayerPage.tsx` 真实模式读取 `state.reviewerRouting`、`state.feed` 与 `execution-capabilities`,以 `团队动态 / 决策台 / 角色收件箱 / 智能体工位 / AI 副驾` 展示团队协作态势、待审变更责任路由、执行能力与容量信号、个人执行准备度和工具准备度,只给责任建议、阻塞解释与跳转,不自动分派、不自动审查、不写 ChangeSet 或 Project Memory;决策台与待审路由卡片不得预留独立空白操作列,正文应使用完整卡片宽度,1-2 条路由按双列或单列密度展示;`ReleaseGrowthPage.tsx` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run、摄取 release feedback,并展示真实 feedback、revenue、ROI、compliance 与 recommendation 风险态,从真实 recommendation 单条或同组批量生成下一轮待确认 ChangeSet draft;生成后仍进入 Change Review accept/edit/defer,不自动 commit、不写 Project Memory、不执行部署命令、不接外部支付、不接真实合规平台;所有真实证据和审计状态仍可见,静态高保真发布/增长数据仅保留在显式 demo 模式 - 团队层只读动态与协作态势:`TeamLayerPage.tsx` 直接消费 `ProjectTruthState.feed` 聚合的 review request、review decision、变更集状态、execution progress 与 release gate 事件,并结合 `reviewerRouting`、`execution-capabilities` 和容量计划生成首屏摘要;前端只做空态、摘要、中文化展示和跳转,不写审批、不写 ChangeSet、不写通知,也不在真实模式 fallback 到 mock 动态。 - 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` diff --git a/docs/architecture/RUNTIME_GUARDRAILS.md b/docs/architecture/RUNTIME_GUARDRAILS.md index 1d53cf9f..f5455b01 100644 --- a/docs/architecture/RUNTIME_GUARDRAILS.md +++ b/docs/architecture/RUNTIME_GUARDRAILS.md @@ -47,7 +47,7 @@ - 技术层 fallback 仅允许用于重试、缓存、解析修复、传输兼容等基础设施环节,禁止上浮为用户可见业务结论。 - AI Steward 与其它生成链的成功/失败判定必须优先使用结构校验、字段完整性和证据一致性。 - PMFlow OS `ChangeReviewPage` 真实模式只能展示 `ProjectTruthState.changeSets / changeItems / evidence` 的后端投影;顶部统计、待确认列表和右侧 AI 副驾评估都必须由真实 ChangeItem/Evidence 派生。没有真实待审项时必须显示空态或待补充,禁止用 demo 的知识库搜索、混合召回、固定风险/建议/计数补位。`accepted / edited / deferred` 只表示候选项已被人工处理但尚未写入 Project Memory;只要 ChangeSet 未 committed 且 ChangeItem 未 committed/skipped,就必须允许继续 accept/edit/defer,真正锁定边界是 ChangeSet commit。 -- PMFlow OS 团队审查与执行边界必须保持单向主链:`TeamLayerPage` 只允许消费 `reviewerRouting / reviewDecision / approvalState / TeamFeed` 做责任路由、角色收件箱、团队动态、审查状态、阻塞解释和跳转,不得直接执行 ChangeSet commit、写 Project Memory 或成为第二套 Change Review;Execution Bridge handoff 只能针对 Project Memory commit 后已进入正式项目对象的 Task,禁止从 `pending_review` ChangeItem、候选 Task、reviewer suggestion 或 team routing item 直接创建正式执行分派。`ExecutionBridgePage` 的 Run 工作台只能把 `已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 作为前端流转表达和证据 gate,不得因为 UI 阶段推进而自动验收、自动合并、自动写 Project Memory 或自动进入发布成功。缺 reviewer、repo/runtime、PromptPackage、测试命令、权限或回写证据时必须返回 blocked/warning,禁止冒充已开始开发或已分配成功。 +- PMFlow OS 团队审查与执行边界必须保持单向主链:`TeamLayerPage` 只允许消费 `reviewerRouting / reviewDecision / approvalState / TeamFeed` 做责任路由、角色收件箱、团队动态、审查状态、阻塞解释和跳转,不得直接执行 ChangeSet commit、写 Project Memory 或成为第二套 Change Review;Execution Bridge handoff 只能针对 Project Memory commit 后已进入正式项目对象的 Task,禁止从 `pending_review` ChangeItem、候选 Task、reviewer suggestion 或 team routing item 直接创建正式执行分派。`ExecutionBridgePage` 的开发范围总览和 Run 工作台只能把 Requirement/Task 范围、`已确认需求 / 上下文包 / 开发 / 测试 / 审查合并 / 回写` 阶段作为前端流转表达和证据 gate;未归属任务只能展示为未归属,不得反推需求关系;不得因为 UI 阶段推进而自动验收、自动合并、自动写 Project Memory 或自动进入发布成功。缺 reviewer、repo/runtime、PromptPackage、测试命令、权限或回写证据时必须返回 blocked/warning,禁止冒充已开始开发或已分配成功。 ### 3.3 业务动作归因主链 - 运行时业务动作归因必须优先走: diff --git a/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx b/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx index 7917bdd0..8cd9b15e 100644 --- a/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx +++ b/frontend/src/features/pmflow-os/pages/ExecutionBridgePage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { AlertTriangle, CheckCircle2, @@ -44,9 +44,12 @@ import { } from "@/features/pmflow-os/components/ExecutionBridgeCards"; import { usePMFlowOsStore } from "@/features/pmflow-os/store"; import type { + Requirement as PMFlowRequirement, PMFlowExecutionCapabilityMode, PMFlowExecutionCapabilitySnapshot, PMFlowExecutionModeCapability, + ProjectTruthState, + Task as PMFlowProjectTask, } from "@/features/pmflow-os/types"; import { cn } from "@/lib/utils"; @@ -144,6 +147,51 @@ interface ExecutionRunState { sourceItems: Array<{ label: string; value: string; tone?: "default" | "warning" | "success" }>; } +type ExecutionScopeTone = "ready" | "running" | "waiting" | "blocked" | "empty"; + +interface ExecutionScopeTaskItem { + id: string; + title: string; + summary: string; + status: string; + stage: string; + requirementId?: string | null; + requirementTitle?: string | null; + devTask?: PMFlowDevTask | null; + projectTask?: PMFlowProjectTask | null; + runStage: string; + nextAction: string; + evidenceCount: number; + tone: ExecutionScopeTone; +} + +interface ExecutionScopeRequirementGroup { + id: string; + title: string; + summary: string; + status: string; + tasks: ExecutionScopeTaskItem[]; + coverageLabel: string; + tone: ExecutionScopeTone; +} + +interface ExecutionScopeBoard { + groups: ExecutionScopeRequirementGroup[]; + unassignedTasks: ExecutionScopeTaskItem[]; + allTasks: ExecutionScopeTaskItem[]; + selectedTaskId: string | null; + totals: { + requirements: number; + tasks: number; + running: number; + waitingPackage: number; + waitingTest: number; + waitingReview: number; + blocked: number; + }; + nextQueueAction: string; +} + function isFormalDevTask(task?: PMFlowDevTask | null) { const status = String(task?.status || "").toLowerCase(); return Boolean( @@ -294,9 +342,23 @@ export default function ExecutionBridgePage() { const isLoading = dataSourceMode === "api" && executionSummaryStatus === "loading"; const error = dataSourceMode === "api" ? executionSummaryError : null; const [writebackOpen, setWritebackOpen] = useState(false); - const run = summary ? buildExecutionRunState(summary, capabilities) : null; + const [selectedScopeTaskId, setSelectedScopeTaskId] = useState(null); + const baseScope = useMemo(() => (summary ? buildExecutionScopeBoard(state, summary, capabilities, selectedScopeTaskId) : null), [state, summary, capabilities, selectedScopeTaskId]); + const selectedTaskId = baseScope?.selectedTaskId || null; + const scopedSummary = useMemo( + () => (summary && selectedTaskId ? createScopedExecutionSummary(summary, state, selectedTaskId) : summary), + [summary, state, selectedTaskId] + ); + const scope = useMemo(() => (scopedSummary ? buildExecutionScopeBoard(state, scopedSummary, capabilities, selectedTaskId) : baseScope), [state, scopedSummary, capabilities, selectedTaskId, baseScope]); + const run = scopedSummary ? buildExecutionRunState(scopedSummary, capabilities) : null; + + useEffect(() => { + if (!scope?.selectedTaskId || selectedScopeTaskId) return; + setSelectedScopeTaskId(scope.selectedTaskId); + }, [scope?.selectedTaskId, selectedScopeTaskId]); + const handlePrimaryAction = () => { - if (!summary || !run || !run.primaryAction.enabled) return; + if (!scopedSummary || !run || !run.primaryAction.enabled) return; if (run.primaryAction.kind === "create_handoff" && !isDemo && run.formalTask) { createExecutionHandoff({ task_id: run.formalTask.id, @@ -319,25 +381,26 @@ export default function ExecutionBridgePage() { return; } if (run.primaryAction.kind === "change_review") { - window.location.assign(`/change-review?projectId=${summary.projectId}`); + window.location.assign(`/change-review?projectId=${scopedSummary.projectId}`); return; } if (run.primaryAction.kind === "team_layer") { - window.location.assign(`/team-layer?projectId=${summary.projectId}`); + window.location.assign(`/team-layer?projectId=${scopedSummary.projectId}`); return; } if (run.primaryAction.kind === "release") { - window.location.assign(`/release-growth?projectId=${summary.projectId}`); + window.location.assign(`/release-growth?projectId=${scopedSummary.projectId}`); } }; return (
- {run && summary ? ( + {run && scopedSummary ? ( { refetchExecutionSummary(); @@ -372,38 +435,45 @@ export default function ExecutionBridgePage() { ) : null} - {summary && run ? ( + {scopedSummary && run && scope ? ( <> - {dataSourceMode === "api" && executionCapabilitiesStatus === "loading" ? ( ) : null} {dataSourceMode === "api" && executionCapabilitiesStatus === "error" ? ( ) : null} - + +
- - setWritebackOpen(true)} - onDemoPush={isDemo ? advanceExecution : undefined} - /> - setWritebackOpen(true)} - /> + +
+ + setWritebackOpen(true)} + onDemoPush={isDemo ? advanceExecution : undefined} + /> +
+
+ + setWritebackOpen(true)} + /> +
- + {writebackOpen && !isDemo ? ( setWritebackOpen(false)} /> @@ -418,12 +488,14 @@ export default function ExecutionBridgePage() { function ExecutionRunHeader({ summary, run, + scope, dataSourceMode, onRefresh, onPrimaryAction, }: { summary: PMFlowExecutionSummary; run: ExecutionRunState; + scope?: ExecutionScopeBoard | null; dataSourceMode: "api" | "demo" | "empty"; onRefresh: () => void; onPrimaryAction: () => void; @@ -444,6 +516,7 @@ function ExecutionRunHeader({ {run.task?.title || "等待正式开发任务"} · 当前阶段:{run.currentStage.label}
+ {scope ? : null} @@ -505,13 +578,13 @@ function ExecutionRunStageRail({ run }: { run: ExecutionRunState }) {
-
+
{run.stages.map((stage, index) => ( -
- {index < run.stages.length - 1 ?
: null} +
+ {index < run.stages.length - 1 ?
: null}
+
+
+
本轮开发范围
+

+ 先看所有已确认需求和正式任务,再进入单个 Run;没有关联的任务会进入未归属队列,不反推关系。 +

+
+ 0 ? "warning" : "brand"} /> +
+
+ + + + + + +
+ + ); +} + +function ExecutionScopeQueuePanel({ + scope, + selectedTaskId, + onSelectTask, +}: { + scope: ExecutionScopeBoard; + selectedTaskId: string | null; + onSelectTask: (taskId: string) => void; +}) { + return ( + + ); +} + +function ExecutionScopeTaskButton({ + task, + active, + onClick, +}: { + task: ExecutionScopeTaskItem; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} + function ExecutionRunSourcePanel({ run, summary }: { run: ExecutionRunState; summary: PMFlowExecutionSummary }) { const prompt = run.prompt; return ( @@ -766,12 +962,14 @@ function ExecutionResultCompactPanel({ run }: { run: ExecutionRunState }) { function ExecutionRunDecisionPanel({ run, summary, + scope, capabilities, onPrimaryAction, onOpenWriteback, }: { run: ExecutionRunState; summary: PMFlowExecutionSummary; + scope?: ExecutionScopeBoard | null; capabilities?: PMFlowExecutionCapabilitySnapshot | null; onPrimaryAction: () => void; onOpenWriteback: () => void; @@ -834,6 +1032,17 @@ function ExecutionRunDecisionPanel({
+ {scope ? ( +
+

范围缺口

+
+ + +
+

{scope.nextQueueAction}

+
+ ) : null} +

下一步建议

    @@ -1824,6 +2033,240 @@ function StatusChip({ label, tone }: { label: string; tone: "brand" | "success" ); } +function buildExecutionScopeBoard( + state: ProjectTruthState, + summary: PMFlowExecutionSummary, + capabilities?: PMFlowExecutionCapabilitySnapshot | null, + requestedTaskId?: string | null +): ExecutionScopeBoard { + const taskItems = buildExecutionScopeTasks(state, summary, capabilities); + const requirementMap = new Map(state.requirements.map((item) => [item.id, item])); + const groupedTaskIds = new Set(); + const groups = state.requirements + .filter((requirement) => ["confirmed", "in_progress", "done", "released"].includes(requirement.status)) + .map((requirement) => { + const tasks = taskItems.filter((task) => task.requirementId === requirement.id || requirement.linkedTaskIds.includes(task.id)); + tasks.forEach((task) => groupedTaskIds.add(task.id)); + return buildRequirementScopeGroup(requirement, tasks); + }); + const unassignedTasks = taskItems.filter((task) => !groupedTaskIds.has(task.id) && (!task.requirementId || !requirementMap.has(task.requirementId))); + const allTasks = [...taskItems]; + const selectedTaskId = + (requestedTaskId && allTasks.some((task) => task.id === requestedTaskId) ? requestedTaskId : null) || + (summary.execution.taskId && allTasks.some((task) => task.id === summary.execution.taskId) ? summary.execution.taskId : null) || + firstActionableScopeTask(allTasks)?.id || + allTasks[0]?.id || + null; + const totals = { + requirements: groups.length, + tasks: allTasks.length, + running: allTasks.filter((task) => task.tone === "running" || task.tone === "ready").length, + waitingPackage: allTasks.filter((task) => task.nextAction.includes("上下文包") || task.nextAction.includes("交接")).length, + waitingTest: allTasks.filter((task) => task.nextAction.includes("测试")).length, + waitingReview: allTasks.filter((task) => task.nextAction.includes("审查") || task.nextAction.includes("验收")).length, + blocked: allTasks.filter((task) => task.tone === "blocked").length, + }; + return { + groups, + unassignedTasks, + allTasks, + selectedTaskId, + totals, + nextQueueAction: buildScopeNextAction(totals), + }; +} + +function buildExecutionScopeTasks( + state: ProjectTruthState, + summary: PMFlowExecutionSummary, + capabilities?: PMFlowExecutionCapabilitySnapshot | null +): ExecutionScopeTaskItem[] { + const projectTaskMap = new Map(state.tasks.map((task) => [task.id, task])); + const devTaskMap = new Map(summary.activeDevTasks.map((task) => [task.id, task])); + const taskIds = new Set([ + ...state.tasks.filter((task) => task.stage === "development" || ["in_progress", "blocked", "done"].includes(task.status)).map((task) => task.id), + ...summary.activeDevTasks.map((task) => task.id), + ]); + if (summary.execution.taskId) taskIds.add(summary.execution.taskId); + + return Array.from(taskIds) + .map((taskId) => { + const projectTask = projectTaskMap.get(taskId) || null; + const devTask = devTaskMap.get(taskId) || null; + const task = devTask || (projectTask ? adaptProjectTaskToDevTask(projectTask, state, summary.repoBindings[0] || null) : null); + if (!task) return null; + return buildScopeTaskItem(task, projectTask, devTask, state, summary, capabilities); + }) + .filter(Boolean) as ExecutionScopeTaskItem[]; +} + +function buildScopeTaskItem( + task: PMFlowDevTask, + projectTask: PMFlowProjectTask | null, + devTask: PMFlowDevTask | null, + state: ProjectTruthState, + summary: PMFlowExecutionSummary, + capabilities?: PMFlowExecutionCapabilitySnapshot | null +): ExecutionScopeTaskItem { + const requirement = + (projectTask?.requirementId ? state.requirements.find((item) => item.id === projectTask.requirementId) : null) || + state.requirements.find((item) => item.linkedTaskIds.includes(task.id)) || + null; + const scopedSummary = createScopedExecutionSummary(summary, state, task.id); + const run = buildExecutionRunState(scopedSummary, capabilities); + const evidence = scopedSummary.recentProgressEvents.length + (scopedSummary.promptPackage ? 1 : 0); + const tone = scopeToneFromRun(run); + return { + id: task.id, + title: task.title, + summary: task.description || projectTask?.summary || "任务暂无描述。", + status: task.status || projectTask?.status || "unknown", + stage: projectTask?.stage || "development", + requirementId: requirement?.id || projectTask?.requirementId || task.charter_id || null, + requirementTitle: requirement?.title || task.charter_title || null, + devTask, + projectTask, + runStage: run.currentStage.label, + nextAction: run.primaryAction.label, + evidenceCount: evidence, + tone, + }; +} + +function buildRequirementScopeGroup( + requirement: PMFlowRequirement, + tasks: ExecutionScopeTaskItem[] +): ExecutionScopeRequirementGroup { + const blocked = tasks.some((task) => task.tone === "blocked"); + const running = tasks.some((task) => task.tone === "running" || task.tone === "ready"); + const waiting = tasks.some((task) => task.tone === "waiting"); + const tone: ExecutionScopeTone = blocked ? "blocked" : running ? "running" : waiting ? "waiting" : tasks.length ? "ready" : "empty"; + return { + id: requirement.id, + title: requirement.title, + summary: requirement.summary, + status: requirement.status, + tasks, + coverageLabel: tasks.length ? `${tasks.length} 任务` : "未拆任务", + tone, + }; +} + +function firstActionableScopeTask(tasks: ExecutionScopeTaskItem[]) { + return ( + tasks.find((task) => task.tone === "blocked") || + tasks.find((task) => task.tone === "waiting") || + tasks.find((task) => task.tone === "running") || + tasks.find((task) => task.tone === "ready") || + null + ); +} + +function buildScopeNextAction(totals: ExecutionScopeBoard["totals"]) { + if (totals.blocked > 0) return "先处理阻塞"; + if (totals.waitingPackage > 0) return "生成待执行上下文包"; + if (totals.waitingTest > 0) return "查看待测试任务"; + if (totals.waitingReview > 0) return "进入待审查/验收"; + if (totals.tasks > 0) return "继续跟踪执行证据"; + return "等待正式开发任务"; +} + +function createScopedExecutionSummary( + summary: PMFlowExecutionSummary, + state: ProjectTruthState, + taskId: string +): PMFlowExecutionSummary { + const existingDevTask = summary.activeDevTasks.find((task) => task.id === taskId) || null; + const projectTask = state.tasks.find((task) => task.id === taskId) || null; + const selectedTask = existingDevTask || (projectTask ? adaptProjectTaskToDevTask(projectTask, state, summary.repoBindings[0] || null) : null); + const mergedTasks = selectedTask + ? [selectedTask, ...summary.activeDevTasks.filter((task) => task.id !== selectedTask.id)] + : summary.activeDevTasks; + const scopedEvents = summary.recentProgressEvents.filter((event) => { + const eventTaskId = event.task?.id || null; + if (!eventTaskId) return taskId === summary.execution.taskId; + return eventTaskId === taskId; + }); + const promptMatches = summary.promptPackage && (!summary.promptPackage.task_id || summary.promptPackage.task_id === taskId); + const selectedIsCurrent = taskId === summary.execution.taskId; + const scopedExecution = selectedIsCurrent + ? summary.execution + : { + ...summary.execution, + taskId, + status: scopedEvents.length ? summary.execution.status : "queued" as const, + prStatus: selectedTask?.code_link?.pr_number ? `PR #${selectedTask.code_link.pr_number}` : "pending", + testSummary: "pending", + writebackStatus: "pending", + commitLog: selectedTask?.code_link?.last_commit_sha ? [selectedTask.code_link.last_commit_sha] : [], + }; + return { + ...summary, + execution: scopedExecution, + promptPackage: promptMatches ? summary.promptPackage : null, + activeDevTasks: mergedTasks, + recentProgressEvents: scopedEvents, + }; +} + +function adaptProjectTaskToDevTask( + task: PMFlowProjectTask, + state: ProjectTruthState, + repo: PMFlowRepoBinding | null +): PMFlowDevTask { + const requirement = task.requirementId ? state.requirements.find((item) => item.id === task.requirementId) : null; + return { + id: task.id, + title: task.title, + description: task.summary, + status: task.status, + priority: null, + project_id: state.project.id, + charter_id: requirement?.id || task.requirementId || null, + charter_title: requirement?.title || null, + assignee: task.assignee ? { id: task.assignee, name: task.assignee, role: null } : null, + prompt_package_count: 0, + model_binding: null, + code_link: repo + ? { + id: `${task.id}-repo-link`, + status: "pending", + branch_name: null, + pr_number: null, + last_commit_sha: null, + repo, + } + : null, + }; +} + +function scopeToneFromRun(run: ExecutionRunState): ExecutionScopeTone { + if (run.hasFailure || run.currentStage.tone === "blocked") return "blocked"; + if (run.hasAccepted || run.hasMerged) return "ready"; + if (run.hasHandoff || run.hasCommit || run.hasPr || run.hasTest) return "running"; + if (!run.prompt || !run.repo || !run.formalTask) return "waiting"; + return "ready"; +} + +function scopeToneToChip(tone: ExecutionScopeTone): "brand" | "success" | "warning" | "risk" | "muted" { + if (tone === "ready") return "success"; + if (tone === "running") return "brand"; + if (tone === "waiting") return "warning"; + if (tone === "blocked") return "risk"; + return "muted"; +} + +function taskScopeToneLabel(tone: ExecutionScopeTone) { + const map: Record = { + ready: "就绪", + running: "推进中", + waiting: "待补齐", + blocked: "阻塞", + empty: "空", + }; + return map[tone]; +} + function buildExecutionRunState( summary: PMFlowExecutionSummary, capabilities?: PMFlowExecutionCapabilitySnapshot | null diff --git a/frontend/src/features/pmflow-os/store.api.test.tsx b/frontend/src/features/pmflow-os/store.api.test.tsx index d9756a90..858e328f 100644 --- a/frontend/src/features/pmflow-os/store.api.test.tsx +++ b/frontend/src/features/pmflow-os/store.api.test.tsx @@ -706,6 +706,40 @@ describe("PMFlowOsProvider real change review actions", () => { }); it("renders execution run workspace with staged flow and a single primary action", async () => { + const scopedState = realState(); + scopedState.requirements = [ + { + id: "req-031", + title: "REQ-031 执行范围", + summary: "本轮需要一起推进的已确认开发需求。", + stage: "development", + status: "confirmed", + linkedTaskIds: ["task-99", "task-extra"], + }, + ]; + scopedState.tasks = [ + { + id: "task-99", + title: "真实任务", + summary: "真实任务描述", + stage: "development", + status: "in_progress", + assignee: "Owner", + acceptanceCriteria: ["已有执行证据"], + requirementId: "req-031", + }, + { + id: "task-extra", + title: "补充企业权限说明", + summary: "同一需求下的另一个开发任务。", + stage: "development", + status: "confirmed", + assignee: "Owner", + acceptanceCriteria: ["权限边界明确"], + requirementId: "req-031", + }, + ]; + getTruthStateMock.mockResolvedValueOnce(scopedState); getExecutionCapabilitiesMock.mockResolvedValueOnce({ projectId: "123", executionModes: [ @@ -766,6 +800,10 @@ describe("PMFlowOsProvider real change review actions", () => { expect(await screen.findByText("开发执行流转")).toBeInTheDocument(); expect(screen.getByText("开发执行 Run 工作台")).toBeInTheDocument(); + expect(screen.getByText("本轮开发范围")).toBeInTheDocument(); + expect(screen.getByText("需求执行队列")).toBeInTheDocument(); + expect(screen.getAllByText("REQ-031 执行范围").length).toBeGreaterThan(0); + expect(screen.getByRole("button", { name: /补充企业权限说明/ })).toBeInTheDocument(); expect(screen.getByText("执行源包")).toBeInTheDocument(); expect(screen.getByText("AI 副驾")).toBeInTheDocument(); expect(screen.getAllByText("AI 直接执行").length).toBeGreaterThan(0); @@ -777,6 +815,9 @@ describe("PMFlowOsProvider real change review actions", () => { fireEvent.click(screen.getAllByRole("button", { name: /回写开发 \/ 测试证据/ })[0]); expect(screen.getByText("回写执行证据")).toBeInTheDocument(); expect(screen.getAllByText("运行端已领取,等待 branch、commit、PR 或测试结果回流。").length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole("button", { name: /补充企业权限说明/ })); + expect(screen.getAllByText("补充企业权限说明").length).toBeGreaterThan(0); + expect(screen.getAllByRole("button", { name: /生成上下文包/ }).length).toBeGreaterThan(0); }); it("renders Change Review from real projection and does not show demo fallback rows", async () => {