diff --git a/.graphlemon/runs/837fc183-f87f-41d9-8bc9-6262d7172834/code_diff.patch b/.graphlemon/runs/837fc183-f87f-41d9-8bc9-6262d7172834/code_diff.patch
new file mode 100644
index 0000000..4eb2a9f
--- /dev/null
+++ b/.graphlemon/runs/837fc183-f87f-41d9-8bc9-6262d7172834/code_diff.patch
@@ -0,0 +1,458 @@
+diff --git a/frontend/app.js b/frontend/app.js
+index da24c1e..2f17236 100644
+--- a/frontend/app.js
++++ b/frontend/app.js
+@@ -97,6 +97,13 @@ const routes = {
+ const TASK_DETAIL_RE = /^#\/tasks\/([^/]+)$/;
+ const SKILL_AUDIT_RE = /^#\/skills\/([^/]+)\/audit$/;
+ const ARTIFACT_DETAIL_RE = /^#\/artifacts\/([^/]+)$/;
++// Org Graph 三节点条点击可达详情 (2026-08-06 SDD): both v_dashboard's and
++// v_task_detail's org-strip nodes route here. `:org_node` is restricted to
++// the three real values on purpose -- anything else (typo, stray path
++// segment) falls through to the "Not found" fallback in render() instead of
++// reaching renderTaskDetail/mountTaskDetail with a bogus org_node that would
++// otherwise need its own defensive checks further down.
++const ORG_NODE_DETAIL_RE = /^#\/tasks\/([^/]+)\/org\/(planner|coder|validator)$/;
+
+ let currentRouteCleanup = null;
+ function registerRouteCleanup(fn) {
+@@ -114,9 +121,15 @@ function render() {
+ let routeArgs = [];
+ if (!route) {
+ const taskMatch = hash.match(TASK_DETAIL_RE);
++ const orgNodeMatch = hash.match(ORG_NODE_DETAIL_RE);
+ const skillMatch = hash.match(SKILL_AUDIT_RE);
+ const artifactMatch = hash.match(ARTIFACT_DETAIL_RE);
+- if (taskMatch) {
++ if (orgNodeMatch) {
++ // Reuses v_task_detail itself (same render/mount) with the org_node
++ // carried through as a second arg -- no separate view/route handler.
++ route = { render: renderTaskDetail, mount: mountTaskDetail };
++ routeArgs = [orgNodeMatch[1], orgNodeMatch[2]];
++ } else if (taskMatch) {
+ route = { render: renderTaskDetail, mount: mountTaskDetail };
+ routeArgs = [taskMatch[1]];
+ } else if (skillMatch) {
+@@ -537,6 +550,120 @@ function mountBugFeedback() {
+
+ const ORG_NODE_ORDER = ["planner", "coder", "validator"];
+
++// ---- Org Graph 三节点条点击可达详情 (2026-08-06 SDD): shared by
++// v_dashboard's `#org_strip` and v_task_detail's own inline org-strip below
++// -- both mark each `.org-node` with `data-org-node`, both route through
++// `#/tasks/:task_id/org/:org_node`, both pick "最新一次 work_graph_run" for
++// that org_node the same way (C6: plain array filtering, no new backend
++// query params). DAG/log/artifact display itself stays entirely inside
++// renderGraphVisualizerPanel/renderTodoTree/mountLogViewer/v_artifact_browser
++// (C3-C5) -- this block only does the "接线": which run is "latest", and
++// where on the already-rendered page to scroll/highlight.
++
++function getLatestRunForOrgNode(task, orgNode) {
++ const runs = task.work_graph_runs.filter((r) => r.org_node === orgNode);
++ return runs.length ? runs[runs.length - 1] : null;
++}
++
++// v_dashboard's org-strip: role + hint text, only ever links out to
++// v_task_detail (a genuine page change, so plain hash assignment is fine).
++function renderDashboardOrgStripHtml(activeOrgNode, runStatusByOrgNode) {
++ return ORG_NODE_ORDER.map((node, i) => {
++ const isActive = activeOrgNode === node;
++ const runStatus = runStatusByOrgNode[node];
++ const hint = isActive ? "当前节点" : runStatus === "done" ? "已完成" : runStatus || "未开始";
++ const arrow = i < ORG_NODE_ORDER.length - 1 ? '
→
' : "";
++ return ''
++ + node + '
' + escapeHtml(hint) + "
" + arrow;
++ }).join("");
++}
++
++// v_task_detail's inline "Org Graph" panel: plainer markup (no role/hint
++// split), `activeSet` already carries different semantics than the
++// dashboard's single activeOrgNode -- kept as its own function rather than
++// forcing the two call sites to share one shape.
++function renderTaskDetailOrgStripHtml(activeSet) {
++ return ORG_NODE_ORDER.map((node, i) => {
++ const arrow = i < ORG_NODE_ORDER.length - 1 ? '→
' : "";
++ return '' + node + "
" + arrow;
++ }).join("");
++}
++
++function mountOrgStripClicks(container, taskId) {
++ if (!taskId) return;
++ for (const el of container.querySelectorAll(".org-node[data-org-node]")) {
++ el.addEventListener("click", () => {
++ window.location.hash = "#/tasks/" + taskId + "/org/" + el.dataset.orgNode;
++ });
++ }
++}
++
++// "组合视图" content for v_task_detail's "Org 节点详情" panel: reuses the
++// artifact list's own markup pattern (li[data-artifact-id], same as
++// renderArtifactSummaryList) filtered down to this run's own artifacts --
++// linking to #/artifacts/:id (v_artifact_browser), not re-rendering artifact
++// content itself (C5). The DAG/to-do-tree/log panels are NOT duplicated here
++// -- they're already rendered once, further down this same page, by the
++// unmodified renderGraphVisualizerPanel/renderTodoTree calls; this function
++// only describes which run that is and lists its artifacts.
++function renderOrgComboPanelContent(task, orgNode) {
++ const run = getLatestRunForOrgNode(task, orgNode);
++ if (!run) {
++ return '' + escapeHtml(orgNode) + " 尚未开始(暂无 Work Graph run)
";
++ }
++ const hasDag = !!run.graph_manifest && run.graph_manifest.nodes.length > 0;
++ const artifacts = task.artifacts.filter((a) => a.produced_by_run_id === run.run_id);
++ const artifactsHtml = artifacts.length ? (
++ '' + artifacts.map((a) =>
++ '- ' + escapeHtml(a.file_path) + ""
++ + escapeHtml(a.artifact_type) + " · " + escapeHtml(a.validation_status) + "
"
++ ).join("") + "
"
++ ) : '暂无 Artifact
';
++ return (
++ '最新一次 run_id=' + escapeHtml(run.run_id) + "(status=" + escapeHtml(run.status) + ")"
++ + (hasDag ? " · 已定位到下方 Work Graph / to-do 树,点击节点可展开日志" : " · 该 run 尚未生成 Work Graph") + "
"
++ + '该 run 产出的 Artifact
'
++ + artifactsHtml
++ );
++}
++
++function wireOrgComboArtifactClicks(scopeEl) {
++ const combo = scopeEl.querySelector("#org_combo_content");
++ if (!combo) return;
++ for (const li of combo.querySelectorAll("li[data-artifact-id]")) {
++ li.addEventListener("click", () => {
++ window.location.hash = "#/artifacts/" + encodeURIComponent(li.dataset.artifactId);
++ });
++ }
++}
++
++// Called both on initial mount (deep link / dashboard click landed here with
++// an org_node already known) and from the inline org-strip's own click
++// handler (already on this page -- 02-V happy path 2.2 explicitly wants an
++// in-page scroll, not another full page render). Scrolls to the DAG panel
++// first (renders whenever graph_manifest exists), falls back to the to-do
++// tree panel, then to the combo panel itself for the "尚无 run"/"尚未生成
++// Work Graph" empty states -- never throws if a target is missing.
++function focusOrgNodePanel(content, task, orgNode) {
++ for (const el of content.querySelectorAll(".org-strip .org-node[data-org-node]")) {
++ el.classList.toggle("org-node-focused", el.dataset.orgNode === orgNode);
++ }
++ const comboContent = content.querySelector("#org_combo_content");
++ if (comboContent) {
++ comboContent.innerHTML = renderOrgComboPanelContent(task, orgNode);
++ wireOrgComboArtifactClicks(content);
++ }
++ const run = getLatestRunForOrgNode(task, orgNode);
++ let target = run ? document.getElementById("graph-panel-" + run.run_id) : null;
++ if (!target && run) target = document.getElementById("todo-tree-" + run.run_id);
++ if (!target) target = content.querySelector("#org_combo_panel");
++ if (target && target.scrollIntoView) {
++ target.scrollIntoView({ behavior: "smooth", block: "start" });
++ target.classList.add("org-highlight-flash");
++ setTimeout(() => target.classList.remove("org-highlight-flash"), 1600);
++ }
++}
++
+ function renderDashboard() {
+ if (!getCurrentProjectId()) return renderNoProjectSelected();
+ return `
+@@ -585,16 +712,13 @@ function mountDashboard() {
+ const quotaBanner = document.getElementById("quota_info_banner");
+ const wsBanner = document.getElementById("ws_disconnected_banner");
+ const recentEvents = [];
++ // Set right before every renderOrgStrip() call below -- org-strip clicks
++ // need to know which task_id to route to (#/tasks/:task_id/org/:org_node).
++ let orgStripTaskId = null;
+
+ function renderOrgStrip(activeOrgNode, runStatusByOrgNode) {
+- orgStrip.innerHTML = ORG_NODE_ORDER.map((node, i) => {
+- const isActive = activeOrgNode === node;
+- const runStatus = runStatusByOrgNode[node];
+- const hint = isActive ? "当前节点" : runStatus === "done" ? "已完成" : runStatus || "未开始";
+- const arrow = i < ORG_NODE_ORDER.length - 1 ? '→
' : "";
+- return ''
+- + node + '
' + escapeHtml(hint) + "
" + arrow;
+- }).join("");
++ orgStrip.innerHTML = renderDashboardOrgStripHtml(activeOrgNode, runStatusByOrgNode);
++ mountOrgStripClicks(orgStrip, orgStripTaskId);
+ }
+
+ async function refreshTasksAndOrgGraph() {
+@@ -602,11 +726,13 @@ function mountDashboard() {
+ const tasks = await resp.json();
+ const active = tasks.find((t) => t.status !== "done") || tasks[0];
+ if (!active) {
++ orgStripTaskId = null;
+ orgStrip.innerHTML = '暂无运行中任务
';
+ taskSummary.innerHTML = '';
+ quotaBanner.style.display = "none";
+ return;
+ }
++ orgStripTaskId = active.task_id;
+ // v_dashboard Start affordance (2026-08-03): the full Pause/Stop set
+ // only makes sense on v_task_detail (they need the task's own status
+ // context to pick which one to show) -- the dashboard only ever needs
+@@ -888,7 +1014,10 @@ function renderGraphVisualizerPanel(run, options) {
+ }).join("");
+
+ return (
+- '' + run.org_node + " Work Graph DAG 视图(v_graph_visualizer,run_id=" + run.run_id + ")
"
++ // id="graph-panel-
": Org Graph 点击可达详情's scroll/highlight
++ // target (focusOrgNodePanel above) -- additive, doesn't change any
++ // existing class/data-* this function already emitted.
++ '' + run.org_node + " Work Graph DAG 视图(v_graph_visualizer,run_id=" + run.run_id + ")
"
+ + '
' + rowsHtml + "
"
+ + '
' + GRAPH_LEGEND_HTML + "
"
+ + "
"
+@@ -1031,11 +1160,19 @@ function renderTaskDetail(taskId) {
+ `;
+ }
+
+-function mountTaskDetail(taskId) {
++function mountTaskDetail(taskId, initialOrgNode) {
+ const projectId = getCurrentProjectId();
+ if (!projectId) return;
+ const content = document.getElementById("td_content");
+
++ // Org Graph 三节点条点击可达详情 (2026-08-06 SDD): which org_node's
++ // "组合视图" (DAG/日志/Artifact) is currently focused -- seeded from the
++ // #/tasks/:task_id/org/:org_node route (deep link / dashboard click) and
++ // updated in place when the inline org-strip below is clicked while
++ // already on this page, so a WS-triggered refresh() re-render keeps
++ // pointing at the same node instead of losing focus.
++ let focusedOrgNode = initialOrgNode || null;
++
+ // v_log_viewer instances (02-V §2.7.1) mounted lazily on first to-do
+ // expand -- tracked here so a re-render (WS-triggered refresh) or leaving
+ // the page closes any live c_ws_log_tail connections instead of leaking them.
+@@ -1069,7 +1206,9 @@ function mountTaskDetail(taskId) {
+ );
+ }).join("");
+ return (
+- '' + run.org_node + " Work Graph 的 to-do 树(run_id=" + run.run_id + ")
"
++ // id="todo-tree-
": secondary scroll/highlight target when the
++ // DAG panel above didn't render (graph_manifest missing).
++ '' + run.org_node + " Work Graph 的 to-do 树(run_id=" + run.run_id + ")
"
+ + '
"
+ + '
DAG 视图(同一份 graph_manifest 的另一种呈现)见下方 v_graph_visualizer
'
+ + "
"
+@@ -1082,10 +1221,7 @@ function mountTaskDetail(taskId) {
+ for (const run of task.work_graph_runs) {
+ if (run.status !== "done" && run.status !== "failed") activeSet.add(run.org_node);
+ }
+- const orgStripHtml = ORG_NODE_ORDER.map((node, i) => {
+- const arrow = i < ORG_NODE_ORDER.length - 1 ? '→
' : "";
+- return '' + node + "
" + arrow;
+- }).join("");
++ const orgStripHtml = renderTaskDetailOrgStripHtml(activeSet);
+
+ // v_task_detail Start/Pause/Stop (2026-08-03): a task only ever has one
+ // real thing "running" against it at a time -- exactly one of these
+@@ -1168,6 +1304,13 @@ function mountTaskDetail(taskId) {
+ + "CREATED_VIA" + escapeHtml(task.created_via) + "
"
+ + " "
+ + 'Org Graph
' + orgStripHtml + "
"
++ // "Org 节点详情" (2026-08-06 SDD): click-through target -- filled in by
++ // focusOrgNodePanel() below, either right after this render (deep
++ // link/dashboard click already carries an org_node) or on a later
++ // inline org-strip click. Left as the default hint until then.
++ + 'Org 节点详情
'
++ + '
点击上方 Org Graph 节点,查看该节点最新一次 Work Graph / 日志 / Artifact
'
++ + "
"
+ + todoTreesHtml
+ + graphVisualizerHtml
+ + ''
+@@ -1191,6 +1334,22 @@ function mountTaskDetail(taskId) {
+ }
+ mountGraphVisualizerTooltips(content);
+
++ // Org Graph 三节点条点击可达详情: clicking a node here (already on
++ // v_task_detail) stays on this page -- update focusedOrgNode + the URL
++ // (history.replaceState, no hashchange -> no full route re-render) and
++ // scroll/highlight in place, per 02-V happy path 2.2.
++ for (const el of content.querySelectorAll(".org-strip .org-node[data-org-node]")) {
++ el.addEventListener("click", () => {
++ focusedOrgNode = el.dataset.orgNode;
++ const newHash = "#/tasks/" + taskId + "/org/" + focusedOrgNode;
++ if (window.location.hash !== newHash) history.replaceState(null, "", newHash);
++ focusOrgNodePanel(content, task, focusedOrgNode);
++ });
++ }
++ // Deep link / dashboard click landed here with an org_node already
++ // known -- populate + scroll to it once, right after this render.
++ if (focusedOrgNode) focusOrgNodePanel(content, task, focusedOrgNode);
++
+ const resumeBtn = document.getElementById("btn_manual_resume");
+ if (resumeBtn) {
+ resumeBtn.addEventListener("click", async () => {
+diff --git a/frontend/style.css b/frontend/style.css
+index 1ec077d..c3b4179 100644
+--- a/frontend/style.css
++++ b/frontend/style.css
+@@ -45,6 +45,18 @@ body { font-family: system-ui, sans-serif; margin: 0; }
+ .org-node .role { font-weight: 600; font-size: 13px; }
+ .org-node .hint { margin-top: 2px; }
+ .org-arrow { flex: 0 0 30px; text-align: center; color: #6b7280; }
++
++/* Org Graph 三节点条点击可达详情 (2026-08-06 SDD): org-strip nodes route to
++ `#/tasks/:task_id/org/:org_node` (v_dashboard) or scroll/highlight in
++ place (v_task_detail's own inline org-strip). */
++.org-strip .org-node[data-org-node] { cursor: pointer; }
++.org-strip .org-node[data-org-node]:hover { box-shadow: 0 0 0 2px #4f46e5; }
++.org-strip .org-node.org-node-focused { box-shadow: 0 0 0 2px #4f46e5; }
++.org-highlight-flash { animation: org-highlight-flash-kf 1.6s ease-out; }
++@keyframes org-highlight-flash-kf {
++ 0% { box-shadow: 0 0 0 3px #f59e0b; }
++ 100% { box-shadow: none; }
++}
+ .task-summary .row { display: flex; justify-content: space-between; font-size: 13px; padding: 6px 0; border-bottom: 1px solid #e5e7eb; }
+ .task-summary .row:last-child { border-bottom: none; }
+ .progress { height: 8px; border-radius: 4px; background: #e5e7eb; overflow: hidden; margin-top: 6px; }
+diff --git a/frontend/test_org_click_through_smoke.js b/frontend/test_org_click_through_smoke.js
+new file mode 100644
+index 0000000..043319b
+--- /dev/null
++++ b/frontend/test_org_click_through_smoke.js
+@@ -0,0 +1,144 @@
++// Org Graph 三节点条点击可达详情 (2026-08-06 SDD) smoke test. Same
++// no-framework/no-fixture convention as frontend/test_graph_console_smoke.js
++// -- loads app.js in a bare vm.Context and asserts on generated HTML
++// strings / regex matches / plain data for the new top-level functions:
++// - renderDashboardOrgStripHtml / renderTaskDetailOrgStripHtml: both
++// org-strip render locations now carry data-org-node on every node
++// (root cause from the BRD: renderOrgStrip() had zero data-* attrs).
++// - ORG_NODE_DETAIL_RE: the new #/tasks/:task_id/org/:org_node route --
++// legal three-way org_node enum, illegal node name, missing segment.
++// - getLatestRunForOrgNode: "取最新 run" filter -- must return the
++// started_at-latest (array-last) entry for a given org_node, not the
++// first, and must not merge two runs' nodes together.
++// - renderOrgComboPanelContent: empty-state (no run yet) and populated
++// (with/without artifacts) cases, always via the existing simple-list
++// markup pattern -- no separate artifact-content renderer invented here.
++//
++// Run: node frontend/test_org_click_through_smoke.js
++
++"use strict";
++const fs = require("fs");
++const path = require("path");
++const vm = require("vm");
++const assert = require("assert");
++
++function makeFakeDiv() {
++ let text = "";
++ return {
++ set textContent(v) { text = v == null ? "" : String(v); },
++ get textContent() { return text; },
++ get innerHTML() {
++ return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
++ },
++ };
++}
++
++const sandbox = {
++ document: {
++ createElement: () => makeFakeDiv(),
++ getElementById: () => null,
++ querySelectorAll: () => [],
++ addEventListener: () => {},
++ },
++ window: {
++ location: { protocol: "http:", host: "localhost", hash: "" },
++ addEventListener: () => {},
++ },
++ localStorage: { getItem: () => null, setItem: () => {} },
++ console,
++};
++vm.createContext(sandbox);
++
++const src = fs.readFileSync(path.join(__dirname, "app.js"), "utf8");
++// Top-level `const` regexes stay lexically scoped to the script and never
++// reach the sandbox object (same Annex B quirk test_graph_console_smoke.js
++// already documents for ARTIFACT_DETAIL_RE) -- re-export what this test needs.
++const augmentedSrc = src + "\nglobalThis.ORG_NODE_DETAIL_RE = ORG_NODE_DETAIL_RE;\n";
++new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox);
++
++let assertionCount = 0;
++function check(condition, message) {
++ assertionCount++;
++ assert.ok(condition, message);
++}
++
++// ---- 1. data-org-node on both org-strip render locations ----
++{
++ const dashboardHtml = sandbox.renderDashboardOrgStripHtml("coder", { planner: "done", coder: "running" });
++ for (const node of ["planner", "coder", "validator"]) {
++ check(dashboardHtml.includes('data-org-node="' + node + '"'), "dashboard org-strip must carry data-org-node for " + node);
++ }
++ check(dashboardHtml.includes('class="org-node active" data-org-node="coder"'), "active node keeps its existing .active class alongside the new attribute");
++
++ const activeSet = new Set(["planner"]);
++ const taskDetailHtml = sandbox.renderTaskDetailOrgStripHtml(activeSet);
++ for (const node of ["planner", "coder", "validator"]) {
++ check(taskDetailHtml.includes('data-org-node="' + node + '"'), "v_task_detail inline org-strip must carry data-org-node for " + node);
++ }
++}
++
++// ---- 2. ORG_NODE_DETAIL_RE: happy / boundary / failure ----
++{
++ const legal = "#/tasks/abc-123/org/coder".match(sandbox.ORG_NODE_DETAIL_RE);
++ check(legal, "legal task_id + coder must match");
++ check(legal[1] === "abc-123" && legal[2] === "coder", "captured groups must be task_id and org_node");
++
++ for (const node of ["planner", "coder", "validator"]) {
++ check(("#/tasks/t1/org/" + node).match(sandbox.ORG_NODE_DETAIL_RE), "all three org_node enum values must match: " + node);
++ }
++
++ check(!"#/tasks/t1/org/reviewer".match(sandbox.ORG_NODE_DETAIL_RE), "illegal org_node (not in the 3-way enum) must not match");
++ check(!"#/tasks/t1/org/".match(sandbox.ORG_NODE_DETAIL_RE), "missing org_node segment must not match");
++ check(!"#/tasks/t1".match(sandbox.ORG_NODE_DETAIL_RE), "plain #/tasks/:id (no /org/ suffix) must not match this regex");
++}
++
++// ---- 3. getLatestRunForOrgNode: latest-wins, no cross-run merging ----
++{
++ const task = {
++ work_graph_runs: [
++ { run_id: "run-old", org_node: "planner", status: "done", started_at: "2026-08-01 00:00:00" },
++ { run_id: "run-new", org_node: "planner", status: "done", started_at: "2026-08-02 00:00:00" },
++ { run_id: "run-coder", org_node: "coder", status: "running", started_at: "2026-08-02 01:00:00" },
++ ],
++ };
++ const latestPlanner = sandbox.getLatestRunForOrgNode(task, "planner");
++ check(latestPlanner.run_id === "run-new", "must return the array-last (time-latest) run for a repeated org_node, not the first");
++
++ const latestCoder = sandbox.getLatestRunForOrgNode(task, "coder");
++ check(latestCoder.run_id === "run-coder", "single-run org_node resolves to that one run");
++
++ const noValidator = sandbox.getLatestRunForOrgNode(task, "validator");
++ check(noValidator === null, "org_node with zero runs must resolve to null, not throw or return undefined-shaped data");
++}
++
++// ---- 4. renderOrgComboPanelContent: empty state + artifact filtering ----
++{
++ const emptyTask = { work_graph_runs: [], artifacts: [] };
++ const emptyHtml = sandbox.renderOrgComboPanelContent(emptyTask, "validator");
++ check(emptyHtml.includes("尚未开始"), "no-run org_node must render an explicit empty state, not blank/undefined");
++
++ const taskWithRun = {
++ work_graph_runs: [
++ { run_id: "run-1", org_node: "coder", status: "done", graph_manifest: { nodes: [{ node_id: "n1", depends_on: [] }] } },
++ ],
++ artifacts: [
++ { artifact_id: "art-1", produced_by_run_id: "run-1", file_path: "src/x.py", artifact_type: "code_diff", validation_status: "passed" },
++ { artifact_id: "art-2", produced_by_run_id: "run-other", file_path: "src/y.py", artifact_type: "code_diff", validation_status: "passed" },
++ ],
++ };
++ const populatedHtml = sandbox.renderOrgComboPanelContent(taskWithRun, "coder");
++ check(populatedHtml.includes("run-1"), "combo content must name the resolved run_id");
++ check(populatedHtml.includes('data-artifact-id="art-1"'), "artifact produced by the resolved run must be listed");
++ check(!populatedHtml.includes('data-artifact-id="art-2"'), "artifact from a different run_id must be filtered out");
++
++ const taskNoArtifacts = {
++ work_graph_runs: [{ run_id: "run-2", org_node: "coder", status: "failed", graph_manifest: null }],
++ artifacts: [],
++ };
++ const noArtifactsHtml = sandbox.renderOrgComboPanelContent(taskNoArtifacts, "coder");
++ check(noArtifactsHtml.includes("暂无 Artifact"), "run with zero artifacts must show the existing empty-state text, not blank/error");
++}
++
++console.log(
++ "frontend/test_org_click_through_smoke.js: all " + assertionCount + " assertions passed"
++);