diff --git a/apps/dashboard/app/pm/components/PMIntakeLeftSidebar.tsx b/apps/dashboard/app/pm/components/PMIntakeLeftSidebar.tsx index c666459..fc77738 100644 --- a/apps/dashboard/app/pm/components/PMIntakeLeftSidebar.tsx +++ b/apps/dashboard/app/pm/components/PMIntakeLeftSidebar.tsx @@ -48,6 +48,8 @@ export default function PMIntakeLeftSidebar(props: Props) { onFocusInput, } = props; const activeSession = sessionHistory.find((session) => session.pm_session_id === intakeId); + const showBlockingHistoryError = Boolean(sessionHistoryError) && (Boolean(intakeId) || sessionHistory.length > 0); + const showFirstRunHistoryNotice = Boolean(sessionHistoryError) && !showBlockingHistoryError; const activeSessionLabel = intakeId ? `${locale === "zh-CN" ? "当前会话" : "Current session"}: ${ activeSession @@ -119,7 +121,14 @@ export default function PMIntakeLeftSidebar(props: Props) { {activeSessionLabel}

- {sessionHistoryError &&

{sessionHistoryError}

} + {showBlockingHistoryError ?

{sessionHistoryError}

: null} + {showFirstRunHistoryNotice ? ( +

+ {locale === "zh-CN" + ? "会话历史暂时不可用。你仍然可以直接发出第一条请求,系统会创建正式会话。" + : "Session history is temporarily unavailable. You can still start the first request and create the formal session."} +

+ ) : null} {newConversationError && (

{newConversationError} diff --git a/apps/dashboard/app/workflows/page.tsx b/apps/dashboard/app/workflows/page.tsx index 1a1d171..230024d 100644 --- a/apps/dashboard/app/workflows/page.tsx +++ b/apps/dashboard/app/workflows/page.tsx @@ -239,7 +239,7 @@ export default async function WorkflowsPage() { {workflowListPageCopy.emptyTitle} {workflowListPageCopy.emptyHint} diff --git a/apps/dashboard/tests/home_page.test.tsx b/apps/dashboard/tests/home_page.test.tsx index 5bf8c9e..9e62690 100644 --- a/apps/dashboard/tests/home_page.test.tsx +++ b/apps/dashboard/tests/home_page.test.tsx @@ -567,6 +567,14 @@ describe("dashboard home run-summary clarity", () => { expect(String(zhMetadata.description)).toContain("证明与回放桌面"); }); + it("renders the English runs desk with English-first copy", async () => { + render(await RunsPage({ searchParams: Promise.resolve({}) })); + + expect(screen.getByRole("heading", { name: "Proof & Replay" })).toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "证明与回放" })).not.toBeInTheDocument(); + expect(screen.getByText("This desk should answer one question first: which run deserves proof right now.")).toBeInTheDocument(); + }); + it("shows degraded risk summary when runs data is unavailable", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); mockFetchRuns.mockRejectedValueOnce(new Error("runs down")); diff --git a/apps/dashboard/tests/pm_page_chat.test.tsx b/apps/dashboard/tests/pm_page_chat.test.tsx index afc4baf..8a5a3a6 100644 --- a/apps/dashboard/tests/pm_page_chat.test.tsx +++ b/apps/dashboard/tests/pm_page_chat.test.tsx @@ -769,7 +769,10 @@ describe("pm page chat-driven flow", () => { mockFetchPmSessions.mockRejectedValue(new Error("network down")); render(); - expect(await screen.findByRole("alert")).toHaveTextContent("Failed to load session history: network error, please try again."); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + expect( + await screen.findByText("Session history is temporarily unavailable. You can still start the first request and create the formal session.") + ).toBeInTheDocument(); }); it("cancels in-flight chat request and keeps input draft", async () => { diff --git a/apps/dashboard/tests/pm_page_stage_flow.suite.tsx b/apps/dashboard/tests/pm_page_stage_flow.suite.tsx index e7d1d53..6ab17a4 100644 --- a/apps/dashboard/tests/pm_page_stage_flow.suite.tsx +++ b/apps/dashboard/tests/pm_page_stage_flow.suite.tsx @@ -372,6 +372,52 @@ describe("pm intake component branches", () => { expect(screen.getByText("Loading session history")).toBeInTheDocument(); }); + it("keeps first-run session history failures as a non-blocking status note", () => { + render( + , + ); + + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + expect( + screen.getByText("Session history is temporarily unavailable. You can still start the first request and create the formal session.") + ).toBeInTheDocument(); + expect(screen.getByText("No previous sessions yet. Send the first request to start.")).toBeInTheDocument(); + }); + + it("keeps session-history failures blocking once a real session exists", () => { + render( + , + ); + + expect(screen.getByRole("alert")).toHaveTextContent("Failed to load session history: network error, please try again."); + }); + it("covers center panel keyboard link actions and inline chain expand action", () => { const onLayoutModeChange = vi.fn(); const onHoveredChainRoleChange = vi.fn(); diff --git a/apps/dashboard/tests/workflows_queue_page.test.tsx b/apps/dashboard/tests/workflows_queue_page.test.tsx index 030c3b4..fb91f83 100644 --- a/apps/dashboard/tests/workflows_queue_page.test.tsx +++ b/apps/dashboard/tests/workflows_queue_page.test.tsx @@ -138,4 +138,16 @@ describe("workflows queue page", () => { expect(screen.getByRole("link", { name: /打开 PM 入口|Open PM intake/ })).toHaveAttribute("href", "/pm"); expect(screen.getByText(/No workflow cases yet|当前还没有工作流案例/)).toBeInTheDocument(); }); + + it("keeps the English empty-state CTA in English", async () => { + vi.mocked(fetchWorkflows).mockResolvedValue([] as never); + vi.mocked(fetchQueue).mockResolvedValue([] as never); + vi.mocked(mutationExecutionCapability).mockReturnValue({ executable: false, operatorRole: null } as never); + + const view = await WorkflowsPage(); + render(view); + + expect(screen.getByRole("link", { name: "Open PM intake" })).toHaveAttribute("href", "/pm"); + expect(screen.queryByRole("link", { name: "打开 PM 入口" })).not.toBeInTheDocument(); + }); }); diff --git a/apps/orchestrator/tests/test_ci_current_run_report_contracts.py b/apps/orchestrator/tests/test_ci_current_run_report_contracts.py index 7f5f563..28f4849 100644 --- a/apps/orchestrator/tests/test_ci_current_run_report_contracts.py +++ b/apps/orchestrator/tests/test_ci_current_run_report_contracts.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os import subprocess import sys from datetime import datetime, timezone @@ -246,6 +247,33 @@ def test_current_run_consistency_downgrades_stale_local_advisory_to_advisory(tmp assert "source_sha_mismatch" in payload["authority_reasons"] +def test_ci_control_plane_doctor_can_emit_current_run_source_metadata(tmp_path: Path) -> None: + out_dir = tmp_path / "doctor" + env = { + "OPENVIBECODING_CI_CONTROL_PLANE_DOCTOR_OUT_DIR": str(out_dir), + "OPENVIBECODING_DOCTOR_REQUIRE_DOCKER": "0", + "OPENVIBECODING_DOCTOR_REQUIRE_SUDO": "0", + "RUNNER_TEMP": str(tmp_path / "runner-temp"), + "OPENVIBECODING_CI_SOURCE_RUN_ID": "local-run", + "OPENVIBECODING_CI_SOURCE_ROUTE": "local_full_ci", + "OPENVIBECODING_CI_SOURCE_EVENT": "local", + } + proc = subprocess.run( + ["bash", str(REPO_ROOT / "scripts" / "ci_control_plane_doctor.sh")], + cwd=REPO_ROOT, + text=True, + capture_output=True, + env={**os.environ, **env}, + check=False, + ) + + assert proc.returncode == 0, proc.stderr or proc.stdout + payload = json.loads((out_dir / "report.json").read_text(encoding="utf-8")) + assert payload["source_run_id"] == "local-run" + assert payload["source_route"] == "local_full_ci" + assert payload["source_event"] == "local" + + def test_artifact_index_marks_head_mismatch_non_authoritative(tmp_path: Path) -> None: route_report = tmp_path / "trusted_pr.json" route_report.write_text("{}", encoding="utf-8") diff --git a/configs/env.registry.json b/configs/env.registry.json index 6cde37a..f92c431 100644 --- a/configs/env.registry.json +++ b/configs/env.registry.json @@ -1692,6 +1692,45 @@ "scripts/lib/ci_main_impl.sh" ] }, + { + "name": "OPENVIBECODING_CI_SOURCE_EVENT", + "scope": "ci", + "secret": false, + "required": false, + "default": null, + "owner": "platform", + "description": "Current CI source event propagated into control-plane doctor and current-run provenance receipts so authoritative local/full-ci evidence can carry the triggering event class.", + "consumers": [ + "scripts/ci_control_plane_doctor.sh", + "scripts/lib/ci_step126_helpers.sh" + ] + }, + { + "name": "OPENVIBECODING_CI_SOURCE_ROUTE", + "scope": "ci", + "secret": false, + "required": false, + "default": null, + "owner": "platform", + "description": "Current CI source route identifier propagated into control-plane doctor and current-run provenance receipts so authoritative evidence keeps the originating route contract.", + "consumers": [ + "scripts/ci_control_plane_doctor.sh", + "scripts/lib/ci_step126_helpers.sh" + ] + }, + { + "name": "OPENVIBECODING_CI_SOURCE_RUN_ID", + "scope": "ci", + "secret": false, + "required": false, + "default": null, + "owner": "platform", + "description": "Current CI source run identifier propagated into control-plane doctor and current-run provenance receipts so authoritative evidence keeps the originating run lineage.", + "consumers": [ + "scripts/ci_control_plane_doctor.sh", + "scripts/lib/ci_step126_helpers.sh" + ] + }, { "name": "OPENVIBECODING_CI_RUNNER_CLASS", "scope": "ci", diff --git a/packages/frontend-shared/uiCopy.js b/packages/frontend-shared/uiCopy.js index f120a36..57d192d 100644 --- a/packages/frontend-shared/uiCopy.js +++ b/packages/frontend-shared/uiCopy.js @@ -581,7 +581,7 @@ const UI_COPY = { queueSummary: (count, slaState) => `queue: ${count} / SLA ${slaState}`, }, runsPage: { - title: "证明与回放", + title: "Proof & Replay", subtitle: "Use this spine to inspect run evidence, compare posture, and replay decisions. Failed-run triage is only one lane inside the broader proof desk.", countsBadge: (runCount) => `${runCount} runs`, warningTitle: "Proof & Replay is currently running with partial truth.", diff --git a/packages/frontend-shared/uiCopy.ts b/packages/frontend-shared/uiCopy.ts index 47e2955..efa0cd1 100644 --- a/packages/frontend-shared/uiCopy.ts +++ b/packages/frontend-shared/uiCopy.ts @@ -1676,7 +1676,7 @@ const UI_COPY: Record = { queueSummary: (count: number, slaState: string) => `queue: ${count} / SLA ${slaState}`, }, runsPage: { - title: "证明与回放", + title: "Proof & Replay", subtitle: "Use this spine to inspect run evidence, compare posture, and replay decisions. Failed-run triage is only one lane inside the broader proof desk.", countsBadge: (runCount: number) => `${runCount} runs`, diff --git a/scripts/ci_control_plane_doctor.sh b/scripts/ci_control_plane_doctor.sh index 827f77a..ca2a08c 100644 --- a/scripts/ci_control_plane_doctor.sh +++ b/scripts/ci_control_plane_doctor.sh @@ -46,8 +46,11 @@ if check_cmd curl; then curl_ok=1; fi if [[ -n "${RUNNER_TEMP:-}" ]]; then runner_temp_ok=1; fi allowlist_json='["OPENVIBECODING_DOC_GATE_MODE","OPENVIBECODING_DOC_GATE_BASE_SHA","OPENVIBECODING_DOC_GATE_HEAD_SHA","OPENVIBECODING_EXTERNAL_WEB_PROBE_PROVIDER_API_MODE","OPENVIBECODING_CI_LIVE_PREFLIGHT_PROVIDER_API_MODE","OPENVIBECODING_CI_EXTERNAL_WEB_PROBE_PROVIDER_API_MODE"]' +SOURCE_RUN_ID="${OPENVIBECODING_CI_SOURCE_RUN_ID:-}" +SOURCE_ROUTE="${OPENVIBECODING_CI_SOURCE_ROUTE:-}" +SOURCE_EVENT="${OPENVIBECODING_CI_SOURCE_EVENT:-}" -python3 - "$REPORT_JSON" "$REPORT_MD" "$docker_ok" "$sudo_ok" "$jq_ok" "$curl_ok" "$runner_temp_ok" "$tool_cache_ok" "$allowlist_json" <<'PY' +python3 - "$REPORT_JSON" "$REPORT_MD" "$docker_ok" "$sudo_ok" "$jq_ok" "$curl_ok" "$runner_temp_ok" "$tool_cache_ok" "$allowlist_json" "$SOURCE_RUN_ID" "$SOURCE_ROUTE" "$SOURCE_EVENT" <<'PY' import json import sys from datetime import datetime, timezone @@ -63,6 +66,9 @@ from pathlib import Path runner_temp_ok, tool_cache_ok, allowlist_json, + source_run_id, + source_route, + source_event, ) = sys.argv[1:] payload = { @@ -77,6 +83,9 @@ payload = { "tool_cache_contract": tool_cache_ok == "1", }, "strict_ci_openvibecoding_allowlist": json.loads(allowlist_json), + "source_run_id": source_run_id, + "source_route": source_route, + "source_event": source_event, } Path(report_json).write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") Path(report_md).write_text( @@ -90,6 +99,9 @@ Path(report_md).write_text( f"- curl: **{payload['checks']['curl']}**", f"- runner_temp: **{payload['checks']['runner_temp']}**", f"- tool_cache_contract: **{payload['checks']['tool_cache_contract']}**", + f"- source_run_id: `{payload['source_run_id']}`", + f"- source_route: `{payload['source_route']}`", + f"- source_event: `{payload['source_event']}`", f"- strict_ci_openvibecoding_allowlist: `{', '.join(payload['strict_ci_openvibecoding_allowlist'])}`", "", ] diff --git a/scripts/lib/ci_step126_helpers.sh b/scripts/lib/ci_step126_helpers.sh index bbadf5c..3b984a2 100644 --- a/scripts/lib/ci_step126_helpers.sh +++ b/scripts/lib/ci_step126_helpers.sh @@ -23,6 +23,13 @@ run_ci_step126_current_run_fanin() { --github-ref "${GITHUB_REF:-local}" \ --github-event-name "${GITHUB_EVENT_NAME:-local}" \ --job-observed "release-evidence" + local_runner_temp="${RUNNER_TEMP:-${CI_REPORT_ROOT}/runner_temp}" + mkdir -p "${local_runner_temp}" + OPENVIBECODING_CI_SOURCE_RUN_ID="${GITHUB_RUN_ID:-local-run}" \ + OPENVIBECODING_CI_SOURCE_ROUTE="${CI_ROUTE_ID}" \ + OPENVIBECODING_CI_SOURCE_EVENT="${GITHUB_EVENT_NAME:-local}" \ + RUNNER_TEMP="${local_runner_temp}" \ + bash scripts/ci_control_plane_doctor.sh declare -a CI_SOURCE_MANIFEST_ARGS=( --output "${CI_SOURCE_MANIFEST_PATH}" --route-id "${CI_ROUTE_ID}" diff --git a/scripts/truth_triage.sh b/scripts/truth_triage.sh index 163e7b2..532136f 100644 --- a/scripts/truth_triage.sh +++ b/scripts/truth_triage.sh @@ -7,6 +7,7 @@ cd "$ROOT_DIR" CURRENT_RUN_ROOT=".runtime-cache/openvibecoding/reports/ci/current_run" CURRENT_RUN_ROUTE_REPORT=".runtime-cache/openvibecoding/reports/ci/routes/local-advisory.json" CURRENT_RUN_SOURCE_MANIFEST="${CURRENT_RUN_ROOT}/source_manifest.json" +CURRENT_RUN_CONSISTENCY_JSON="${CURRENT_RUN_ROOT}/consistency.json" cleanup_forbidden_python_residue() { local found=0 @@ -56,6 +57,27 @@ build_local_advisory_current_run_manifest() { --route-report "$CURRENT_RUN_ROUTE_REPORT" } +preserve_authoritative_current_run_manifest() { + python3 - <<'PY' +import json +from pathlib import Path + +manifest = Path(".runtime-cache/openvibecoding/reports/ci/current_run/source_manifest.json") +consistency = Path(".runtime-cache/openvibecoding/reports/ci/current_run/consistency.json") +if not manifest.is_file() or not consistency.is_file(): + raise SystemExit(1) + +manifest_payload = json.loads(manifest.read_text(encoding="utf-8")) +consistency_payload = json.loads(consistency.read_text(encoding="utf-8")) +is_authoritative = bool(consistency_payload.get("authoritative_current_truth")) +source_route = str(manifest_payload.get("source_route") or "").strip() + +if is_authoritative and source_route and source_route != "local-advisory": + raise SystemExit(0) +raise SystemExit(1) +PY +} + echo "🧭 [truth-triage] start" echo echo "== normalize transient residue ==" @@ -71,7 +93,11 @@ bash scripts/run_governance_py.sh scripts/check_upstream_same_run_cohesion.py echo echo "== current-run truth ==" -build_local_advisory_current_run_manifest +if preserve_authoritative_current_run_manifest; then + echo "ℹ️ [truth-triage] preserving existing authoritative current-run manifest" +else + build_local_advisory_current_run_manifest +fi bash scripts/run_governance_py.sh scripts/check_ci_current_run_sources.py --source-manifest "$CURRENT_RUN_SOURCE_MANIFEST" echo