feat: add execution result writeback protocol

This commit is contained in:
Albert 2026-04-27 16:39:56 +08:00
parent 37056f8fac
commit f48a4d0381
10 changed files with 157 additions and 10 deletions

View File

@ -202,6 +202,8 @@ class PMFlowExecutionProgressEventRequest(BaseModel):
commit_sha: Optional[str] = Field(default=None, max_length=255)
pr_number: Optional[int] = Field(default=None, ge=1)
test_summary: Optional[str] = Field(default=None, max_length=4000)
blocker_summary: Optional[str] = Field(default=None, max_length=4000)
execution_status: Optional[PMFlowExecutionStatus] = None
workflow_state: Optional[str] = Field(default=None, max_length=120)
summary: Optional[str] = Field(default=None, max_length=4000)
event_source_id: Optional[str] = Field(default=None, max_length=255)

View File

@ -4223,9 +4223,29 @@ class PMFlowOSProjectionService:
body: PMFlowExecutionProgressEventRequest,
repo: Optional[RepoBinding],
) -> dict[str, Any]:
payload: dict[str, Any] = {
"workflow_state": body.workflow_state or self._default_workflow_state(body.event_type),
workflow_state = body.workflow_state or body.execution_status or self._default_workflow_state(body.event_type)
repo_full_name = f"{repo.owner}/{repo.repo_name}" if repo is not None else None
execution_result = {
"protocol": "execution_result_protocol_v1",
"eventType": body.event_type,
"source": body.source,
"status": body.execution_status or workflow_state,
"branchName": body.branch_name,
"commitSha": body.commit_sha,
"prNumber": body.pr_number,
"testSummary": body.test_summary,
"blockerSummary": body.blocker_summary,
"repositoryFullName": repo_full_name,
}
payload: dict[str, Any] = {
"protocol_version": "execution_result_protocol_v1",
"workflow_state": workflow_state,
"source": body.source,
"execution_result": {
key: value
for key, value in execution_result.items()
if value is not None and (not isinstance(value, str) or value.strip())
},
"boundary": "PMFlow records execution result metadata only; command execution stays in Local Bridge, Codex, GitHub agent, or manual runtime.",
}
for key, value in {
@ -4233,6 +4253,8 @@ class PMFlowOSProjectionService:
"commit_sha": body.commit_sha,
"pr_number": body.pr_number,
"test_summary": body.test_summary,
"blocker_summary": body.blocker_summary,
"execution_status": body.execution_status,
"event_source_id": body.event_source_id,
}.items():
if value is not None and str(value).strip():
@ -4251,6 +4273,8 @@ class PMFlowOSProjectionService:
) -> str:
if body.summary:
return body.summary
if body.blocker_summary:
return body.blocker_summary
if body.test_summary:
return body.test_summary
repo_label = f"{repo.owner}/{repo.repo_name}" if repo is not None else "未绑定仓库"

View File

@ -1119,8 +1119,15 @@ async def test_execution_progress_writeback_records_branch_pr_commit_and_test(db
assert link.branch_name == "codex/main-flow-d3"
assert link.pr_number == 77
assert link.last_commit_sha == "abc123def456"
events = (await session.execute(select(ProgressEvent).where(ProgressEvent.task_id == task.id))).scalars().all()
events = (
await session.execute(select(ProgressEvent).where(ProgressEvent.task_id == task.id).order_by(ProgressEvent.id.asc()))
).scalars().all()
assert [event.event_type for event in events] == ["commit", "pr_opened", "test_failed"]
assert events[0].payload["protocol_version"] == "execution_result_protocol_v1"
assert events[0].payload["execution_result"]["branchName"] == "codex/main-flow-d3"
assert events[0].payload["execution_result"]["commitSha"] == "abc123def456"
assert events[1].payload["execution_result"]["prNumber"] == 77
assert events[2].payload["execution_result"]["testSummary"] == "单测失败:ExecutionBridgePage 回写表单缺少断言"
@pytest.mark.asyncio
@ -1251,7 +1258,8 @@ async def test_release_gate_blocks_explicit_failed_event(db_env) -> None:
"task_id": str(task.id),
"event_type": "failed",
"source": "codex",
"summary": "Codex 执行失败:typecheck 未通过",
"execution_status": "failed",
"blocker_summary": "Codex 执行失败:typecheck 未通过",
},
)
@ -1261,6 +1269,9 @@ async def test_release_gate_blocks_explicit_failed_event(db_env) -> None:
assert payload["releaseGate"]["status"] == "blocked"
assert "typecheck 未通过" in " ".join(payload["releaseGate"]["blockingReasons"])
assert payload["releaseGate"]["evidence"][0]["type"] == "failed"
event = (await session.execute(select(ProgressEvent).where(ProgressEvent.task_id == task.id))).scalar_one()
assert event.payload["execution_result"]["status"] == "failed"
assert event.payload["execution_result"]["blockerSummary"] == "Codex 执行失败:typecheck 未通过"
@pytest.mark.asyncio

View File

@ -2,6 +2,27 @@
## PMFlow OS
### POST `/pmflow-os/projects/{project_id}/execution-progress-events`
- Purpose: record Codex / IDE / Local Bridge / GitHub agent execution results after an execution handoff has been consumed.
- Auth: project member, `OWNER`, or `REVIEWER`; tenant comes from JWT.
- Request body:
- `task_id: string` required; must belong to the current project.
- `event_type: "branch_created" | "commit" | "pr_opened" | "pr_synced" | "test_passed" | "test_failed" | "checks_passed" | "failed" | "blocked" | "merged" | "accepted" | "manual_progress"`.
- `source?: "pmflow_os" | "local_bridge" | "codex" | "github_agent" | "manual"`.
- `repo_binding_id?: string | null`.
- `branch_name?: string | null`, `commit_sha?: string | null`, `pr_number?: number | null`.
- `test_summary?: string | null`, `blocker_summary?: string | null`.
- `execution_status?: "queued" | "pushed" | "coding" | "pr_open" | "testing" | "synced" | "failed"`.
- `workflow_state?: string | null`, `summary?: string | null`, `event_source_id?: string | null`, `occurred_at?: string | null`.
- Behavior:
- Creates a `ProgressEvent` only; it does not run commands, push branches, create PRs, publish releases, or create formal tasks.
- Stores a backward-compatible flat payload plus `protocol_version: "execution_result_protocol_v1"` and `execution_result` envelope with `{eventType, source, status, branchName, commitSha, prNumber, testSummary, blockerSummary, repositoryFullName}`.
- Returns the latest `ExecutionSummaryProjection`, allowing `execution`, `releaseGate`, and Team Feed projection to consume the same standardized result metadata.
- `failed / blocked / test_failed` events fail-closed into `releaseGate.status="blocked"`; `test_passed / checks_passed` can move the gate to `ready` or `needs_confirmation` depending on PR/sync evidence.
- Failure semantics:
- `404 task_not_found`
- `422 repo_binding_not_found`
### POST `/pmflow-os/projects/{project_id}/extractions`
- Purpose: turn a project thread message into an open ChangeSet with pending_review ChangeItems.
- Auth: project member, `OWNER`, or `REVIEWER`; tenant comes from JWT.

View File

@ -212,6 +212,7 @@
- `frontend/src/features/project-launch/types.ts`、`mockData.ts`、`useProjectLaunchState.ts` 是新项目启动台状态层;默认真实模式通过 `frontend/src/api/pmflowLaunch.ts` 创建 `pmflow_launch_draft_jobs`、轮询 job 状态、拿到草案后再确认创建真实项目,API 失败进入明确失败态,不静默 fallback 成 mock 成功;`mockData.ts` 仅在显式 `?demo=1` 或 `?dataSource=demo` 下作为演示状态机使用,真实模式的步骤摘要、操作流、审查建议、附件引用和计数从当前输入、job 状态、真实草案、warnings/evidence 派生。`components/LaunchShell.tsx` 负责桌面/移动分流,桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,并通过真实补充输入弹层提交 `merge_supplement` revision,禁止硬编码追加示例文本;`SpaceSidebar` 承载稳定空间目的地(空间首页、暂存想法、草案箱、所有项目和管理配置),不展示最近项目、待我确认或 AI 正在推进等首页工作台区块项;真实模式下若 `AIReadiness.canGenerateProjectDraft=false`,优先渲染 `AISetupRequiredPanel`,普通成员只记录想法,空间负责人查看空间 AI 可用状态,平台 AI Ops 才进入平台 AI 能力处理,不提供空项目创建入口;低信息输入由后端 `input_needs_more_info` 422 拦截并进入补充输入失败态。`LaunchCanvases.tsx` 和 `MobileLaunchLayout.tsx` 承接高保真图中的意图输入、AI 工作、人类确认、草案修改、生成失败、创建成功过渡与移动端 bottom sheet 交互。
- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=<id>` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `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` 只读消费。
- 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=<id>` 时通过 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。

File diff suppressed because one or more lines are too long

View File

@ -61,7 +61,7 @@ pmflow_evidence_links
| `TestRun` | `model_eval_runs`、`release_validation_runs`、`progress_events(event_type=test*)` 投影 | 否 | `truth-state`、`execution-summary` | dev-sync progress event 或 release validation | `pending/running -> passed/failed/blocked` | 测试日志和 PR/commit 可作为 evidence | 当前没有通用 test_runs 表,V1 先按投影聚合 |
| `Release` | `truth_nodes.node_type='release'` + `release_validation_runs` | 否 | `truth-state`;SSTC release APIs | SSTC release 创建;`run_release_validation` | `check_pending -> ready -> shipping -> live/watch` | release gate、rollback_plan、validation_summary | 发布对象和发布验证结果需要在 projection 中分清 |
| `MetricSignal` | `feedback_ingests.metrics`、`revenue_events`、`roi_snapshots`、增长埋点结果投影 | 否 | `truth-state`;`/roi-snapshots` | `feedback-ingest`、`revenue-events` | `draft/proposed -> confirmed` | 指标来源和采样窗口必须展示 | 只做轻量反馈,不引入完整财务系统 |
| `ExecutionContext` | `prompt_packages`、`repo_bindings`、`task_code_links`、`progress_events`、`dev_sessions` | 否 | `GET /pmflow-os/projects/{project_id}/execution-summary` 聚合 `GET /projects/{project_id}/dev-sync/summary` | `POST /pmflow-os/projects/{project_id}/execution-handoffs` 生成/复用 `PromptPackage` 并创建 `ProgressEvent` 起点;repo 绑定仍走 dev-sync/GitHub App | `queued -> pushed -> coding -> pr_open -> testing -> synced/failed` | progress events、deliveries、repo/PR/commit 是执行证据 | 不得重造 dev-sync;PMFlow OS 只做项目内执行投影和 handoff contract,不直接执行本机命令 |
| `ExecutionContext` | `prompt_packages`、`repo_bindings`、`task_code_links`、`progress_events`、`dev_sessions` | 否 | `GET /pmflow-os/projects/{project_id}/execution-summary` 聚合 `GET /projects/{project_id}/dev-sync/summary` | `POST /pmflow-os/projects/{project_id}/execution-handoffs` 生成/复用 `PromptPackage` 并创建 `ProgressEvent` 起点;`POST /pmflow-os/projects/{project_id}/execution-progress-events` 写入 `execution_result_protocol_v1` 执行结果;repo 绑定仍走 dev-sync/GitHub App | `queued -> pushed -> coding -> pr_open -> testing -> synced/failed` | progress events、deliveries、repo/PR/commit/test/blocker 是执行证据 | 不得重造 dev-sync;PMFlow OS 只做项目内执行投影和 handoff/result contract,不直接执行本机命令 |
| `TeamFeed / TeamInbox` | `pmflow_change_sets`、`pmflow_approvals`、`progress_events`、release/feedback 事件聚合 | 暂不新增专表,先 projection | `truth-state` | 由各业务动作写入源表 | feed item 不承载主状态 | 必须能追溯到源对象 | Phase F 才补 Role Inbox/Decision Desk 的真实模型 |
## 3. ProjectTruthState 后端投影
@ -423,6 +423,40 @@ POST /pmflow-os/projects/{project_id}/execution-handoffs
- PMFlow backend 只创建 handoff 合同、PromptPackage 与 progress event,不直接执行本机命令。
- 本地 `stdio`、宿主机配置写入和真实执行必须经过 Local Bridge、Codex、GitHub agent 或兼容 MCP host;PMFlow 后端不得写 `~/.codex/config.toml`。
### 4.9 写回执行结果
```text
POST /pmflow-os/projects/{project_id}/execution-progress-events
```
请求:
```json
{
"task_id": "123",
"repo_binding_id": "456",
"event_type": "pr_opened",
"source": "codex",
"branch_name": "codex/execution-result-protocol-p1",
"commit_sha": "abc123def456",
"pr_number": 77,
"test_summary": "pytest 与 typecheck 通过",
"blocker_summary": null,
"execution_status": "pr_open"
}
```
行为:
- 只记录 Codex / IDE / Local Bridge / GitHub agent 已产生的执行结果,不执行本机命令、不推送、不发布。
- 写入 `ProgressEvent` 时同时保留兼容 flat payload 与最小协议 envelope:`protocol_version='execution_result_protocol_v1'`、`execution_result={eventType, source, status, branchName, commitSha, prNumber, testSummary, blockerSummary, repositoryFullName}`。
- 返回最新 `ExecutionSummaryProjection`,其中 `execution` 展示 branch/commit/PR/test/status,`releaseGate` 基于 passed/failed/blocked/checks 事件推导 `ready / blocked / needs_confirmation / empty`,`truth-state.feed` 后续只读展示执行进展。
- `blocker_summary` 优先作为 failed/blocked 事件的客户可见阻塞原因;`test_summary` 优先作为测试类事件摘要。
边界:
- 该接口是执行结果回写协议,不是任务创建、发布执行或命令代理;缺证据时只能返回 pending/needs_confirmation/blocked,不得伪造测试或发布成功。
执行责任与分派边界:
- 执行交接只能针对已属于当前 project / tenant 的正式 `Task`,不得针对 `pending_review` ChangeItem 直接创建 handoff。
@ -479,7 +513,7 @@ PMFlow OS 不重造开发执行系统。执行桥复用:
- `PromptPackage`:开发上下文包,创建接口为 `POST /projects/{project_id}/charters/{charter_id}/prompt-packages`。
- `RepoBinding / GitHubAppInstallation / GitHubAppRepository`:仓库与 GitHub App 授权状态。
- `TaskCodeLink`:任务与 branch/PR/commit 绑定。
- `ProgressEvent`:commit、PR、test、manual progress 的统一回写事件。
- `ProgressEvent`:branch、commit、PR、test、blocker、manual progress 的统一回写事件;PMFlow OS 写入 `execution_result_protocol_v1` envelope,保留执行状态和外部执行证据。
- `DevSession`:开发活跃会话。
- `GET /projects/{project_id}/dev-sync/summary`:当前执行桥最小真实读路径。

View File

@ -10,11 +10,17 @@ import type {
PMFlowDailyCapacityPlan,
PMFlowExecutionContext,
PMFlowExecutionCapabilitySnapshot,
PMFlowExecutionStatus,
PMFlowReviewDecisionAction,
PMFlowRiskLevel,
ProjectTruthState,
} from "@/features/pmflow-os/types";
export type { PMFlowReviewerRouting, PMFlowReviewerRoutingItem, PMFlowReviewerSuggestion } from "@/features/pmflow-os/types";
export type {
PMFlowExecutionStatus,
PMFlowReviewerRouting,
PMFlowReviewerRoutingItem,
PMFlowReviewerSuggestion,
} from "@/features/pmflow-os/types";
type ApiEnvelope<T> = { code: number; message: string; data: T };
const API_BASE = "/api";
@ -348,6 +354,8 @@ export interface PMFlowExecutionProgressEventRequest {
commit_sha?: string | null;
pr_number?: number | null;
test_summary?: string | null;
blocker_summary?: string | null;
execution_status?: PMFlowExecutionStatus | null;
workflow_state?: string | null;
summary?: string | null;
event_source_id?: string | null;

View File

@ -27,6 +27,7 @@ import type {
PMFlowExecutionProgressSource,
PMFlowExecutionSummary,
PMFlowExecutionRuntimeTarget,
PMFlowExecutionStatus,
PMFlowProgressEvent,
PMFlowRecentDelivery,
PMFlowRepoBinding,
@ -471,6 +472,8 @@ function ExecutionWorkspacePanel({
commit_sha?: string | null;
pr_number?: number | null;
test_summary?: string | null;
blocker_summary?: string | null;
execution_status?: PMFlowExecutionStatus | null;
workflow_state?: string | null;
summary?: string | null;
}) => void;
@ -602,6 +605,8 @@ function ExecutionWritebackPanel({
commit_sha?: string | null;
pr_number?: number | null;
test_summary?: string | null;
blocker_summary?: string | null;
execution_status?: PMFlowExecutionStatus | null;
workflow_state?: string | null;
summary?: string | null;
}) => void;
@ -614,6 +619,8 @@ function ExecutionWritebackPanel({
const [commitSha, setCommitSha] = useState(task?.code_link?.last_commit_sha || "");
const [prNumber, setPrNumber] = useState(task?.code_link?.pr_number ? String(task.code_link.pr_number) : "");
const [testSummary, setTestSummary] = useState("");
const [blockerSummary, setBlockerSummary] = useState("");
const [executionStatus, setExecutionStatus] = useState<PMFlowExecutionStatus | "">("");
const [workflowState, setWorkflowState] = useState("");
const canSubmit = Boolean(task);
@ -665,6 +672,23 @@ function ExecutionWritebackPanel({
<WritebackInput label="PR 编号" value={prNumber} onChange={setPrNumber} placeholder="128" />
<WritebackInput label="流程状态" value={workflowState} onChange={setWorkflowState} placeholder="开发中 / 测试中 / 已回写" />
</div>
<label className="grid gap-1.5 text-xs font-semibold text-slate-600">
执行状态
<select
className="h-9 rounded-lg border border-[#dfe7f3] bg-white px-3 text-sm font-medium text-slate-800 outline-none transition focus:border-[#2463EB]"
value={executionStatus}
onChange={(event) => setExecutionStatus(event.target.value as PMFlowExecutionStatus | "")}
>
<option value="">自动推导</option>
<option value="queued">queued</option>
<option value="pushed">pushed</option>
<option value="coding">coding</option>
<option value="pr_open">pr_open</option>
<option value="testing">testing</option>
<option value="synced">synced</option>
<option value="failed">failed</option>
</select>
</label>
<label className="grid gap-1.5 text-xs font-semibold text-slate-600">
测试摘要
<textarea
@ -674,6 +698,15 @@ function ExecutionWritebackPanel({
placeholder="测试通过、失败原因或检查结果"
/>
</label>
<label className="grid gap-1.5 text-xs font-semibold text-slate-600">
阻塞说明
<textarea
className="min-h-20 resize-y rounded-lg border border-[#dfe7f3] bg-white px-3 py-2 text-sm font-medium leading-6 text-slate-800 outline-none transition placeholder:text-slate-400 focus:border-[#2463EB]"
value={blockerSummary}
onChange={(event) => setBlockerSummary(event.target.value)}
placeholder="如无阻塞可留空;blocked / failed 时写清失败点、影响和下一步"
/>
</label>
</div>
<button
type="button"
@ -685,6 +718,8 @@ function ExecutionWritebackPanel({
onClick={() => {
if (!task) return;
const parsedPr = prNumber.trim() ? Number(prNumber) : null;
const cleanedTestSummary = testSummary.trim();
const cleanedBlockerSummary = blockerSummary.trim();
onRecordProgress({
task_id: task.id,
event_type: eventType,
@ -693,9 +728,11 @@ function ExecutionWritebackPanel({
branch_name: branchName.trim() || null,
commit_sha: commitSha.trim() || null,
pr_number: Number.isFinite(parsedPr) ? parsedPr : null,
test_summary: testSummary.trim() || null,
test_summary: cleanedTestSummary || null,
blocker_summary: cleanedBlockerSummary || null,
execution_status: executionStatus || null,
workflow_state: workflowState.trim() || null,
summary: testSummary.trim() || null,
summary: cleanedBlockerSummary || cleanedTestSummary || null,
});
}}
>

View File

@ -1271,12 +1271,20 @@ describe("PMFlowOsProvider real change review actions", () => {
source: "github_agent",
branch_name: "codex/main-flow-d3",
pr_number: 88,
execution_status: "pr_open",
blocker_summary: null,
})
);
await waitFor(() =>
expect(createExecutionProgressEventMock).toHaveBeenCalledWith(
"123",
expect.objectContaining({ task_id: "task-99", event_type: "pr_opened", source: "github_agent", pr_number: 88 })
expect.objectContaining({
task_id: "task-99",
event_type: "pr_opened",
source: "github_agent",
pr_number: 88,
execution_status: "pr_open",
})
)
);