coding-agent-harness 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.en-US.md +14 -0
  4. package/README.md +230 -80
  5. package/README.zh-CN.md +290 -0
  6. package/SKILL.md +132 -198
  7. package/docs-release/README.md +80 -9
  8. package/docs-release/architecture/overview.md +298 -28
  9. package/docs-release/architecture/overview.zh-CN.md +292 -0
  10. package/docs-release/assets/dashboard-overview.png +0 -0
  11. package/docs-release/assets/harness-architecture.svg +163 -0
  12. package/docs-release/assets/harness-workflow.svg +64 -0
  13. package/docs-release/guides/agent-installation.en-US.md +237 -0
  14. package/docs-release/guides/agent-installation.md +149 -27
  15. package/docs-release/guides/contributing.md +100 -0
  16. package/docs-release/guides/contributing.zh-CN.md +99 -0
  17. package/docs-release/guides/document-audience-and-surfaces.en-US.md +113 -0
  18. package/docs-release/guides/document-audience-and-surfaces.md +113 -0
  19. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
  20. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
  21. package/docs-release/guides/legacy-migration-agent-prompt.md +373 -0
  22. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +350 -0
  23. package/docs-release/guides/migration-playbook.en-US.md +324 -0
  24. package/docs-release/guides/migration-playbook.md +328 -0
  25. package/docs-release/guides/parent-control-repository-pattern.en-US.md +254 -0
  26. package/docs-release/guides/parent-control-repository-pattern.md +254 -0
  27. package/docs-release/guides/preset-development.md +214 -0
  28. package/docs-release/guides/repository-operating-models.en-US.md +197 -0
  29. package/docs-release/guides/repository-operating-models.md +197 -0
  30. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  31. package/docs-release/guides/task-state-machine.md +214 -0
  32. package/docs-release/intl/README.md +15 -0
  33. package/docs-release/intl/de-DE.md +18 -0
  34. package/docs-release/intl/en-US.md +18 -0
  35. package/docs-release/intl/es-ES.md +18 -0
  36. package/docs-release/intl/fr-FR.md +18 -0
  37. package/docs-release/intl/ja-JP.md +18 -0
  38. package/docs-release/intl/ko-KR.md +18 -0
  39. package/docs-release/intl/zh-CN.md +18 -0
  40. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
  41. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  42. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
  43. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
  44. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
  45. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
  46. package/package.json +10 -3
  47. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  48. package/presets/legacy-migration/preset.yaml +134 -0
  49. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  50. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  51. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  52. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  53. package/presets/legacy-migration/templates/review.seed.md +12 -0
  54. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  55. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  56. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  57. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  58. package/presets/lesson-sedimentation/preset.yaml +23 -0
  59. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  60. package/presets/module/preset.yaml +25 -0
  61. package/presets/module/templates/execution_strategy.append.md +8 -0
  62. package/presets/module/templates/task_plan.append.md +17 -0
  63. package/presets/standard-task/preset.yaml +31 -0
  64. package/presets/standard-task/templates/task_plan.append.md +7 -0
  65. package/references/adversarial-review-standard.md +2 -2
  66. package/references/agents-md-pattern.md +5 -5
  67. package/references/delivery-operating-model-standard.md +3 -3
  68. package/references/docs-directory-standard.md +53 -10
  69. package/references/external-source-intake-standard.md +75 -0
  70. package/references/harness-ledger.md +53 -94
  71. package/references/legacy-12-phase-bootstrap.md +41 -0
  72. package/references/lessons-governance.md +100 -88
  73. package/references/module-parallel-standard.md +14 -14
  74. package/references/planning-loop.md +51 -7
  75. package/references/project-onboarding-audit.md +10 -0
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +12 -1
  78. package/references/review-routing-standard.md +7 -1
  79. package/references/ssot-governance.md +67 -59
  80. package/references/taskr-gap-analysis.md +600 -0
  81. package/references/testing-standard.md +50 -0
  82. package/references/walkthrough-closeout.md +10 -9
  83. package/scripts/check-harness.mjs +111 -331
  84. package/scripts/commands/dashboard-command.mjs +67 -0
  85. package/scripts/commands/migration-command.mjs +96 -0
  86. package/scripts/commands/preset-command.mjs +73 -0
  87. package/scripts/commands/task-command.mjs +327 -0
  88. package/scripts/harness.mjs +106 -20
  89. package/scripts/lib/capability-registry.mjs +591 -0
  90. package/scripts/lib/check-module-parallel.mjs +237 -0
  91. package/scripts/lib/check-profiles.mjs +418 -0
  92. package/scripts/lib/check-task-contracts.mjs +47 -0
  93. package/scripts/lib/core-shared.mjs +196 -0
  94. package/scripts/lib/dashboard-data.mjs +412 -0
  95. package/scripts/lib/dashboard-workbench.mjs +257 -0
  96. package/scripts/lib/dashboard-writer.mjs +107 -4
  97. package/scripts/lib/git-status-summary.mjs +46 -0
  98. package/scripts/lib/governance-index-generator.mjs +174 -0
  99. package/scripts/lib/governance-sync.mjs +514 -0
  100. package/scripts/lib/governance-table-boundary.mjs +175 -0
  101. package/scripts/lib/harness-core.mjs +15 -1318
  102. package/scripts/lib/lesson-maintenance.mjs +152 -0
  103. package/scripts/lib/markdown-utils.mjs +158 -0
  104. package/scripts/lib/migration-planner.mjs +478 -0
  105. package/scripts/lib/migration-support.mjs +312 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +497 -0
  108. package/scripts/lib/preset-registry.mjs +627 -0
  109. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  110. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  111. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  112. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  113. package/scripts/lib/task-completion-consistency.mjs +16 -0
  114. package/scripts/lib/task-index.mjs +93 -0
  115. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  116. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  117. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  118. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  119. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  120. package/scripts/lib/task-lifecycle.mjs +649 -0
  121. package/scripts/lib/task-review-model.mjs +469 -0
  122. package/scripts/lib/task-scanner.mjs +576 -0
  123. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  124. package/scripts/postinstall.mjs +14 -0
  125. package/skills/preset-creator/SKILL.md +179 -0
  126. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  127. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  128. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  129. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  130. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  131. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  132. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  133. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  134. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  137. package/{templates/planning/visual_roadmap.md → skills/preset-creator/references/complex-task-skeleton/visual_map.md} +24 -2
  138. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  139. package/templates/AGENTS.md.template +51 -36
  140. package/templates/architecture/Architecture-SSoT.md +21 -0
  141. package/templates/architecture/README.md +49 -0
  142. package/templates/architecture/critical-flows.md +22 -0
  143. package/templates/architecture/local-repo-context.md +20 -0
  144. package/templates/architecture/service-catalog.md +17 -0
  145. package/templates/architecture/services/service-template.md +31 -0
  146. package/templates/architecture/system-map.md +22 -0
  147. package/templates/dashboard/assets/app-src/00-state.js +42 -0
  148. package/templates/dashboard/assets/app-src/10-router.js +77 -0
  149. package/templates/dashboard/assets/app-src/20-overview.js +241 -0
  150. package/templates/dashboard/assets/app-src/30-tasks.js +409 -0
  151. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  152. package/templates/dashboard/assets/app-src/40-modules.js +58 -0
  153. package/templates/dashboard/assets/app-src/45-review.js +347 -0
  154. package/templates/dashboard/assets/app-src/50-migration.js +183 -0
  155. package/templates/dashboard/assets/app-src/60-shared.js +61 -0
  156. package/templates/dashboard/assets/app-src/90-bindings.js +524 -0
  157. package/templates/dashboard/assets/app.css +3107 -300
  158. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  159. package/templates/dashboard/assets/app.js +2068 -306
  160. package/templates/dashboard/assets/app.manifest.json +12 -0
  161. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  162. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  163. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  164. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  165. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  166. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  167. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  168. package/templates/dashboard/assets/i18n.js +531 -44
  169. package/templates/dashboard/assets/mermaid-renderer.js +58 -8
  170. package/templates/development/README.md +52 -0
  171. package/templates/development/codebase-map.md +11 -0
  172. package/templates/development/cross-repo-debugging.md +18 -0
  173. package/templates/development/external-context/service-template.md +33 -0
  174. package/templates/development/external-source-packs/README.md +24 -0
  175. package/templates/development/external-source-packs/digest-template.md +28 -0
  176. package/templates/development/local-setup.md +16 -0
  177. package/templates/development/stubs-and-mocks.md +11 -0
  178. package/templates/integrations/README.md +40 -0
  179. package/templates/integrations/api-contract.md +42 -0
  180. package/templates/integrations/event-contract.md +46 -0
  181. package/templates/integrations/third-party/vendor-template.md +42 -0
  182. package/templates/integrations/webhook-contract.md +41 -0
  183. package/templates/ledger/Harness-Ledger.md +13 -25
  184. package/templates/lessons/lesson-arch-process-change.md +1 -1
  185. package/templates/lessons/lesson-new-doc.md +1 -1
  186. package/templates/lessons/lesson-ref-change.md +1 -1
  187. package/templates/planning/brief.md +32 -0
  188. package/templates/planning/execution_strategy.md +31 -0
  189. package/templates/planning/lesson_candidates.md +70 -0
  190. package/templates/planning/long-running-task-contract.md +7 -0
  191. package/templates/planning/module_brief.md +25 -0
  192. package/templates/planning/module_session_prompt.md +6 -0
  193. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  194. package/templates/planning/optional/references/INDEX.md +3 -3
  195. package/templates/planning/review.md +59 -0
  196. package/templates/planning/task_plan.md +40 -15
  197. package/templates/planning/visual_map.md +50 -0
  198. package/templates/reference/docs-library-standard.md +31 -0
  199. package/templates/reference/execution-workflow-standard.md +5 -2
  200. package/templates/reference/external-source-intake-standard.md +82 -0
  201. package/templates/reference/harness-ledger-standard.md +1 -0
  202. package/templates/reference/pull-request-standard.md +80 -0
  203. package/templates/reference/repo-governance-standard.md +8 -5
  204. package/templates/reference/review-routing-standard.md +6 -0
  205. package/templates/reference/walkthrough-standard.md +3 -1
  206. package/templates/verifier/verifier-output.md +1 -1
  207. package/templates/walkthrough/walkthrough-template.md +2 -2
  208. package/templates-zh-CN/AGENTS.md.template +73 -70
  209. package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
  210. package/templates-zh-CN/architecture/README.md +51 -0
  211. package/templates-zh-CN/architecture/critical-flows.md +24 -0
  212. package/templates-zh-CN/architecture/local-repo-context.md +20 -0
  213. package/templates-zh-CN/architecture/service-catalog.md +17 -0
  214. package/templates-zh-CN/architecture/services/service-template.md +31 -0
  215. package/templates-zh-CN/architecture/system-map.md +22 -0
  216. package/templates-zh-CN/development/README.md +54 -0
  217. package/templates-zh-CN/development/codebase-map.md +11 -0
  218. package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
  219. package/templates-zh-CN/development/external-context/service-template.md +33 -0
  220. package/templates-zh-CN/development/external-source-packs/README.md +24 -0
  221. package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
  222. package/templates-zh-CN/development/local-setup.md +16 -0
  223. package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
  224. package/templates-zh-CN/integrations/README.md +42 -0
  225. package/templates-zh-CN/integrations/api-contract.md +42 -0
  226. package/templates-zh-CN/integrations/event-contract.md +46 -0
  227. package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
  228. package/templates-zh-CN/integrations/webhook-contract.md +41 -0
  229. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  230. package/templates-zh-CN/planning/brief.md +32 -0
  231. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  232. package/templates-zh-CN/planning/lesson_candidates.md +70 -0
  233. package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
  234. package/templates-zh-CN/planning/module_brief.md +25 -0
  235. package/templates-zh-CN/planning/module_plan.md +2 -2
  236. package/templates-zh-CN/planning/module_session_prompt.md +4 -3
  237. package/templates-zh-CN/planning/review.md +59 -1
  238. package/templates-zh-CN/planning/task_plan.md +37 -11
  239. package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
  240. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  241. package/templates-zh-CN/reference/docs-library-standard.md +36 -1
  242. package/templates-zh-CN/reference/execution-workflow-standard.md +10 -2
  243. package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
  244. package/templates-zh-CN/reference/harness-ledger-standard.md +7 -4
  245. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  246. package/templates-zh-CN/reference/repo-governance-standard.md +4 -1
  247. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  248. package/templates-zh-CN/reference/walkthrough-standard.md +6 -5
  249. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
  250. package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
  251. package/scripts/smoke-dashboard.mjs +0 -70
  252. package/scripts/test-harness.mjs +0 -483
  253. package/templates/ssot/Feature-SSoT.md +0 -43
  254. package/templates/ssot/Lessons-SSoT.md +0 -44
  255. package/templates-zh-CN/dashboard/assets/app.css +0 -399
  256. package/templates-zh-CN/dashboard/assets/app.js +0 -435
  257. package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
  258. package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
  259. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
  260. package/templates-zh-CN/dashboard/index.html +0 -18
  261. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  262. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -1,383 +1,1661 @@
1
1
  const bundle = window.__HARNESS_DASHBOARD__ || {};
2
+ const defaultLocale = window.__HARNESS_LOCALE__ || ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en");
3
+ let locale = localStorage.getItem("harness.locale") || defaultLocale;
4
+ if (!window.HarnessI18n?.[locale]) locale = "en";
5
+ let labels = window.HarnessI18n?.[locale] || {};
6
+
2
7
  const state = {
3
- page: "overview",
4
- lang: "en",
5
- theme: localStorage.getItem("harness.theme") || "system",
6
- density: localStorage.getItem("harness.density") || "comfortable",
7
- selected: null,
8
- tab: "plan",
8
+ query: "",
9
+ taskState: "all",
10
+ taskGroupMode: "migration",
11
+ taskPageByGroup: {},
12
+ taskGroupPage: 1,
13
+ warningFilter: "all",
14
+ warningPage: 1,
9
15
  renderMode: "rendered",
16
+ theme: localStorage.getItem("harness.theme") || "system",
17
+ taskLayout: localStorage.getItem("harness.taskLayout") || "list",
18
+ taskSortOrder: localStorage.getItem("harness.taskSortOrder") === "asc" ? "asc" : "desc",
19
+ runtime: { mode: "static", csrfToken: "", writableActions: [] },
20
+ runtimeLoaded: false,
21
+ runtimePoller: null,
10
22
  };
11
23
 
12
- const pageKeys = ["overview", "ledger", "tasks", "modules", "evidence", "lessons", "adoption", "settings"];
24
+ const taskPageSize = 25;
25
+ const taskGroupsPerPage = 8;
26
+ const warningPageSize = 18;
27
+
13
28
  const taskDocTabs = [
14
- ["plan", "task_plan.md"],
29
+ ["brief", "brief.md"],
30
+ ["taskPlan", "task_plan.md"],
15
31
  ["strategy", "execution_strategy.md"],
16
- ["roadmap", "visual_roadmap.md"],
32
+ ["visualMap", "visual_map.md"],
33
+ ["legacyRoadmap", "visual_roadmap.md"],
34
+ ["lessonCandidates", "lesson_candidates.md"],
35
+ ["longRunningContract", "long-running-task-contract.md"],
17
36
  ["progress", "progress.md"],
18
37
  ["review", "review.md"],
19
38
  ["findings", "findings.md"],
39
+ ["walkthrough", "__walkthrough__"],
20
40
  ["references", "references/INDEX.md"],
21
41
  ["artifacts", "artifacts/INDEX.md"],
22
42
  ];
23
43
 
24
44
  function t(key) {
25
- return (window.HarnessI18n?.en || {})[key] || key;
45
+ return labels[key] || key;
46
+ }
47
+
48
+ function setLocale(nextLocale) {
49
+ locale = window.HarnessI18n?.[nextLocale] ? nextLocale : "en";
50
+ labels = window.HarnessI18n?.[locale] || {};
51
+ localStorage.setItem("harness.locale", locale);
26
52
  }
27
53
 
28
54
  function app() {
29
55
  const systemTheme = window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
30
56
  document.documentElement.dataset.theme = state.theme === "system" ? systemTheme : state.theme;
31
- document.documentElement.dataset.density = state.density;
32
- document.documentElement.lang = "en";
57
+ document.documentElement.lang = locale === "zh" ? "zh-CN" : "en";
33
58
  const root = document.getElementById("app");
34
- root.innerHTML = `
35
- <div class="layout">
36
- <aside class="sidebar">
37
- <div class="brand">
38
- <strong>${escapeHtml(bundle.status?.project?.name || "Harness")}</strong>
39
- <span>${escapeHtml(bundle.status?.project?.root || "TARGET:.")}</span>
40
- </div>
41
- <nav class="nav">${pageKeys.map((page) => navButton(page)).join("")}</nav>
42
- </aside>
43
- <main class="main">
44
- ${topbar()}
45
- ${renderPage()}
46
- </main>
47
- </div>`;
59
+ root.innerHTML = shell();
48
60
  bind();
49
61
  }
50
62
 
51
- function navButton(page) {
52
- return `<button data-page="${page}" class="${state.page === page ? "active" : ""}">${t(page)}</button>`;
63
+ function shell() {
64
+ return `<div class="visibility-shell">
65
+ <header class="hero">
66
+ <div class="hero-copy">
67
+ <p class="eyebrow">${t("eyebrow")}</p>
68
+ <h1>${escapeHtml(projectName())} ${t("projectCockpit")}</h1>
69
+ </div>
70
+ <div class="hero-actions">
71
+ ${routeLink("#/", t("overview"), "overview")}
72
+ ${routeLink("#/tasks", t("taskIndex"), "tasks")}
73
+ ${routeLink("#/review", t("reviewQueue"), "review")}
74
+ ${routeLink("#/modules", t("moduleView"), "modules")}
75
+ <button data-language-toggle>${locale === "zh" ? "EN" : "中文"}</button>
76
+ <button data-theme-toggle>${themeLabel()}</button>
77
+ </div>
78
+ </header>
79
+ ${runtimeModeBanner()}
80
+ ${renderRoute()}
81
+ <div id="drawer-overlay" class="drawer-overlay"></div>
82
+ <div id="task-drawer" class="task-drawer"></div>
83
+ </div>`;
53
84
  }
54
85
 
55
- function topbar() {
56
- return `<div class="topbar">
57
- <div>
58
- <p class="eyebrow">Coding Agent Harness Dashboard</p>
59
- <h1>${escapeHtml(pageTitle())}</h1>
86
+ function runtimeModeBanner() {
87
+ if (window.__HARNESS_WORKBENCH__ === true) return "";
88
+ return `<section class="runtime-banner">
89
+ <strong>${t("staticReadOnly")}</strong>
90
+ <span>${t("staticReadOnlyDetail")}</span>
91
+ <code>harness dev</code>
92
+ </section>`;
93
+ }
94
+
95
+ function renderRoute() {
96
+ const route = currentRoute();
97
+ if (route.name === "task") return taskDetail(route);
98
+ if (route.name === "reviewTask") return reviewWorkspace(route);
99
+ if (route.name === "review") return reviewQueue();
100
+ if (route.name === "modules") return modulesView(route.id);
101
+ if (route.name === "tasks") return taskIndex();
102
+ return overview();
103
+ }
104
+
105
+ function currentRoute() {
106
+ const hash = window.location.hash || "#/";
107
+ const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean).map(decodeURIComponent);
108
+ if (parts[0] === "tasks" && parts[1]) return { name: "task", id: parts[1], doc: parts[2] === "docs" ? parts[3] || "" : "" };
109
+ if (parts[0] === "review" && parts[1]) return { name: "reviewTask", id: parts[1] };
110
+ if (parts[0] === "review") return { name: "review" };
111
+ if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
112
+ if (parts[0] === "tasks") return { name: "tasks" };
113
+ return { name: "overview" };
114
+ }
115
+
116
+ function routeLink(hash, text, routeName) {
117
+ const current = currentRoute().name;
118
+ const active = current === routeName || (routeName === "review" && current === "reviewTask");
119
+ return `<a class="${active ? "active" : ""}" href="${hash}">${escapeHtml(text)}</a>`;
120
+ }
121
+
122
+ function overview() {
123
+ return `<div class="dashboard-grid">
124
+ <main class="dashboard-main stack">
125
+ ${flowPanel()}
126
+ ${activeTaskBriefs()}
127
+ ${migrationSummaryPanel()}
128
+ </main>
129
+ <aside class="dashboard-sidebar stack">
130
+ ${statusStrip()}
131
+ ${ledgerPanel()}
132
+ ${healthPanel()}
133
+ ${lessonPanel()}
134
+ </aside>
135
+ </div>`;
136
+ }
137
+
138
+ function statusStrip() {
139
+ const status = bundle.status?.checkState?.status || "unknown";
140
+ const failures = bundle.status?.checkState?.failures || 0;
141
+ const warnings = bundle.status?.checkState?.warnings || 0;
142
+ const tasks = bundle.status?.tasks || [];
143
+ const summary = bundle.status?.summary || {};
144
+ const visual = summary.visualMapCoverage || {};
145
+ const withBrief = tasks.filter((task) => task.briefSource === "standalone").length;
146
+ return `<section class="status-card-group">
147
+ <div class="status-primary ${status}">
148
+ <span>${t("readiness")}</span>
149
+ <strong>${label(status)}</strong>
150
+ <p>${nextActionText()}</p>
60
151
  </div>
61
- <div class="controls">
62
- <div class="control">
63
- <button data-theme="light" class="${state.theme === "light" ? "active" : ""}">${t("light")}</button>
64
- <button data-theme="dark" class="${state.theme === "dark" ? "active" : ""}">${t("dark")}</button>
65
- <button data-theme="system" class="${state.theme === "system" ? "active" : ""}">${t("system")}</button>
152
+ <div class="metrics-grid">
153
+ ${metric(t("tasks"), tasks.length)}
154
+ ${metric(t("briefCoverage"), `${withBrief}/${tasks.length}`)}
155
+ ${metric(t("visualMapCoverage"), `${visual.canonical || 0}/${summary.visualMapRequiredCount || tasks.length}`)}
156
+ ${metric(t("fullCutover"), summary.fullCutoverEligible ? t("ready") : t("notReady"))}
157
+ ${metric(t("legacyVisualOnly"), summary.legacyVisualOnlyCount || 0)}
158
+ ${metric(t("weakBrief"), summary.weakBriefCount || 0)}
159
+ ${metric(t("blockers"), failures)}
160
+ ${metric(t("advice"), warnings)}
161
+ </div>
162
+ </section>`;
163
+ }
164
+
165
+ function metric(labelText, value) {
166
+ return `<div class="metric"><span>${escapeHtml(labelText)}</span><strong>${escapeHtml(value)}</strong></div>`;
167
+ }
168
+
169
+ function nextActionText() {
170
+ const failures = bundle.status?.checkState?.failures || 0;
171
+ if (failures > 0) return t("resolveBlockers");
172
+ const missingBriefs = (bundle.status?.tasks || []).filter((task) => task.briefSource !== "standalone").length;
173
+ if (missingBriefs > 0) return `${missingBriefs} ${t("missingBriefs")}`;
174
+ const warnings = bundle.status?.checkState?.warnings || 0;
175
+ if (warnings > 0) return t("reviewAdvice");
176
+ return t("noBlockers");
177
+ }
178
+
179
+ function flowPanel() {
180
+ const tasks = bundle.status?.tasks || [];
181
+ const total = tasks.length;
182
+ if (total === 0) return "";
183
+ const active = tasks.filter((task) => isActiveTaskState(task.state)).length;
184
+ const done = tasks.filter((task) => !isActiveTaskState(task.state) && (task.state === "done" || task.completion === 100)).length;
185
+ const planned = Math.max(0, total - done - active);
186
+ const pct = (n) => total > 0 ? Math.round((n / total) * 100) : 0;
187
+ return `<section class="flow-panel">
188
+ <div class="section-head">
189
+ <div>
190
+ <p class="eyebrow">${t("firstLook")}</p>
191
+ <h2>${t("projectProgress")}</h2>
66
192
  </div>
67
- <div class="control">
68
- <button data-density="compact" class="${state.density === "compact" ? "active" : ""}">${t("compact")}</button>
69
- <button data-density="comfortable" class="${state.density === "comfortable" ? "active" : ""}">${t("comfortable")}</button>
193
+ <span class="subtle">${done}/${total} ${t("completed")}</span>
194
+ </div>
195
+ <div class="progress-bar-container">
196
+ <div class="progress-bar">
197
+ ${done > 0 ? `<div class="progress-segment done" style="width:${pct(done)}%" title="${t("done")}: ${done}"></div>` : ""}
198
+ ${active > 0 ? `<div class="progress-segment active" style="width:${pct(active)}%" title="${t("active")}: ${active}"></div>` : ""}
199
+ ${planned > 0 ? `<div class="progress-segment planned" style="width:${pct(planned)}%" title="${t("planned")}: ${planned}"></div>` : ""}
200
+ </div>
201
+ <div class="progress-legend">
202
+ <span class="legend-item"><span class="legend-dot done"></span>${t("done")} ${done}</span>
203
+ <span class="legend-item"><span class="legend-dot active"></span>${t("active")} ${active}</span>
204
+ <span class="legend-item"><span class="legend-dot planned"></span>${t("planned")} ${planned}</span>
70
205
  </div>
71
206
  </div>
207
+ ${usesAggregateFlow() ? migrationRunwayBreakdown() : ""}
208
+ </section>`;
209
+ }
210
+
211
+ function projectMermaid() {
212
+ if (usesAggregateFlow()) return migrationAggregateMermaid();
213
+ const graph = bundle.graph || { nodes: [], edges: [] };
214
+ const preferredTypes = graph.nodes?.some((node) => node.type === "module") ? ["module", "step"] : ["task", "phase"];
215
+ const nodes = (graph.nodes || [])
216
+ .filter((node) => preferredTypes.includes(node.type))
217
+ .filter((node) => node.type !== "phase" || ["in_progress", "review", "blocked", "done"].includes(node.state))
218
+ .slice(0, 28);
219
+ if (nodes.length < 2) return mermaidFromBriefs();
220
+ const nodeIds = new Set(nodes.map((node) => node.id));
221
+ const lines = ["flowchart LR"];
222
+ let edgeCount = 0;
223
+ for (const edge of graph.edges || []) {
224
+ if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) continue;
225
+ lines.push(` ${mermaidId(edge.from)}["${mermaidLabel(edge.from)}"] --> ${mermaidId(edge.to)}["${mermaidLabel(edge.to)}"]`);
226
+ edgeCount += 1;
227
+ if (edgeCount >= 34) break;
228
+ }
229
+ if (edgeCount === 0) {
230
+ for (let index = 1; index < nodes.length; index += 1) {
231
+ lines.push(` ${mermaidId(nodes[index - 1].id)}["${mermaidLabel(nodes[index - 1].id)}"] --> ${mermaidId(nodes[index].id)}["${mermaidLabel(nodes[index].id)}"]`);
232
+ }
233
+ }
234
+ return lines.join("\n");
235
+ }
236
+
237
+ function usesAggregateFlow() {
238
+ const graph = bundle.graph || { nodes: [], edges: [] };
239
+ const taskCount = (bundle.status?.tasks || []).length;
240
+ const taskNodes = (graph.nodes || []).filter((node) => node.type === "task").length;
241
+ const usefulEdges = (graph.edges || []).filter((edge) => ["depends_on", "current_step"].includes(edge.type)).length;
242
+ return taskCount > 80 || taskNodes > 80 || ((graph.nodes || []).length > 80 && usefulEdges < 6);
243
+ }
244
+
245
+ function migrationAggregateMermaid() {
246
+ const tasks = bundle.status?.tasks || [];
247
+ const warnings = warningQueue();
248
+ const activeContracts = warnings.filter((warning) => warning.phase === "active-task-contracts").length;
249
+ const moduleCount = new Set(tasks.map(taskModuleKey)).size;
250
+ const reviewWarnings = warnings.filter((warning) => ["review-evidence", "strict-cutover"].includes(warning.phase)).length;
251
+ const lines = [
252
+ "flowchart LR",
253
+ ` baseline["${t("runwayBaseline")}\\n${tasks.length} ${t("tasks")}"] --> triage["${t("runwayTriage")}\\n${warnings.length} ${t("warnings")}"]`,
254
+ ` triage --> contracts["${t("runwayContracts")}\\n${activeContracts} ${t("items")}"]`,
255
+ ` contracts --> modules["${t("runwayModules")}\\n${moduleCount} ${t("groups")}"]`,
256
+ ` modules --> cutover["${t("runwayCutover")}\\n${reviewWarnings} ${t("items")}"]`,
257
+ ];
258
+ return lines.join("\n");
259
+ }
260
+
261
+ function migrationRunwayBreakdown() {
262
+ const tasks = bundle.status?.tasks || [];
263
+ const warnings = warningQueue();
264
+ const phases = [
265
+ ["baseline", t("runwayBaseline"), tasks.length, t("tasks"), "#/tasks"],
266
+ ["triage", t("runwayTriage"), warnings.length, t("warnings"), "#/"],
267
+ ["active-task-contracts", t("runwayContracts"), warnings.filter((warning) => warning.phase === "active-task-contracts").length, t("items"), "#/"],
268
+ ["module-classification", t("runwayModules"), new Set(tasks.map(taskModuleKey)).size, t("groups"), "#/tasks"],
269
+ ["strict-cutover", t("runwayCutover"), warnings.filter((warning) => warning.phase === "strict-cutover").length, t("items"), "#/"],
270
+ ];
271
+ return `<div class="runway-breakdown">
272
+ ${phases.map(([phase, title, count, unit, href]) => `<a href="${href}" data-runway-phase="${escapeAttr(phase)}"><strong>${escapeHtml(title)}</strong><span>${count} ${escapeHtml(unit)}</span></a>`).join("")}
72
273
  </div>`;
73
274
  }
74
275
 
75
- function pageTitle() {
76
- const project = bundle.status?.project?.name || "project";
77
- if (state.page === "overview") return `${project} project cockpit`;
78
- return t(state.page);
276
+ function mermaidFromBriefs() {
277
+ const brief = activeTasks().map((task) => taskDocument(task, "brief.md")).find((doc) => doc?.content?.includes("```mermaid"));
278
+ const match = brief?.content.match(/```mermaid\s*([\s\S]*?)```/i);
279
+ return match ? match[1].trim() : "";
79
280
  }
80
281
 
81
- function renderPage() {
82
- if (state.page === "overview") return overview();
83
- if (state.page === "ledger") return withDrawer(ledgerTable());
84
- if (state.page === "tasks") return withDrawer(taskTable());
85
- if (state.page === "modules") return withDrawer(moduleTable());
86
- if (state.page === "evidence") return withDrawer(evidenceTable());
87
- if (state.page === "lessons") return withDrawer(lessonsTable());
88
- if (state.page === "adoption") return adoption();
89
- return settings();
282
+ function graphSummary() {
283
+ const graph = bundle.graph || { nodes: [], edges: [] };
284
+ if (usesAggregateFlow()) return `${t("aggregateMigrationView")} · ${(bundle.status?.tasks || []).length} ${t("tasks")}`;
285
+ return `${graph.nodes?.length || 0} ${t("nodes")} · ${graph.edges?.length || 0} ${t("edges")}`;
90
286
  }
91
287
 
92
- function overview() {
93
- const status = bundle.status || {};
94
- const evidence = evidenceHealth(status.tasks || []);
95
- const blockers = status.checkState?.failures || 0;
96
- const warnings = status.checkState?.warnings || 0;
97
- return `<section class="status-grid">
98
- <div class="readiness panel">
99
- <span class="state ${status.checkState?.status || "warn"}">${t("readiness")}: ${label(status.checkState?.status || "unknown")}</span>
100
- <h2>${nextActionText()}</h2>
101
- <p class="muted">Blockers, evidence gaps, and adoption advice are separated.</p>
102
- </div>
103
- ${metric(t("activeTasks"), status.tasks?.length || 0)}
104
- ${metric(t("blockers"), blockers)}
105
- ${metric(t("warnings"), warnings)}
106
- </section>
107
- <section class="page-grid">
108
- <div>
109
- ${overviewTasks()}
110
- ${ledgerSummary()}
111
- ${riskPanel()}
288
+ function activeTaskBriefs() {
289
+ const tasks = activeTasks();
290
+ return `<section class="task-briefs">
291
+ <div class="section-head">
292
+ <div>
293
+ <p class="eyebrow">${t("currentWork")}</p>
294
+ <h2>${t("activeBriefs")}</h2>
295
+ </div>
296
+ <div class="section-actions">
297
+ <span class="subtle">${t("activeBriefCount").replace("{count}", tasks.length).replace("{order}", taskSortLabel())}</span>
298
+ <a href="#/tasks">${t("openTaskIndex")}</a>
299
+ </div>
300
+ </div>
301
+ <div class="brief-scroll">
302
+ <div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
112
303
  </div>
113
- ${drawer()}
114
304
  </section>`;
115
305
  }
116
306
 
117
- function metric(label, value) {
118
- return `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`;
307
+ function activeTasks() {
308
+ const tasks = bundle.status?.tasks || [];
309
+ const active = tasks.filter((task) => isActiveTaskState(task.state) || ["planned", "not_started"].includes(task.state));
310
+ if (active.length > 0) return sortTasksByTime(active);
311
+ return sortTasksByTime(tasks.filter((task) => task.briefSource === "standalone"));
119
312
  }
120
313
 
121
- function nextActionText() {
122
- const failures = bundle.status?.checkState?.failures || 0;
123
- if (failures > 0) return "Resolve release blockers first";
124
- const advice = bundle.adoption?.warnings?.length || 0;
125
- if (advice > 0) return "Proceed, with adoption advice to address";
126
- return "No blockers in this snapshot";
127
- }
128
-
129
- function overviewTasks() {
130
- const rows = (bundle.status?.tasks || []).slice(0, 6);
131
- return tablePanel(t("tasks"), [t("task"), t("state"), t("completion"), t("roadmapSource")], rows.map((task) => [
132
- clickable(task.title, "task", task.id),
133
- tag(task.state, label(task.state)),
134
- progress(task.completion),
135
- escapeHtml(label(task.roadmapSource || "unknown")),
136
- ]));
137
- }
138
-
139
- function riskPanel() {
140
- const warnings = bundle.adoption?.warnings || [];
141
- const grouped = groupBy(warnings, (item) => item.category);
142
- return `<section class="panel" style="padding:16px;margin-top:16px">
143
- <h2>Risks and adoption advice</h2>
144
- <div class="risk-list">${Object.entries(grouped).map(([category, items]) => `
145
- <div class="risk-item"><strong>${escapeHtml(category)}</strong><p class="muted">${items.length} items</p></div>
146
- `).join("") || `<p class="empty">No advice</p>`}</div>
314
+ function isActiveTaskState(state) {
315
+ return ["active", "in_progress", "review", "blocked", "reopened", "current-evidence"].includes(state);
316
+ }
317
+
318
+ function taskBriefCard(task, { compact = true } = {}) {
319
+ const doc = taskDocument(task, "brief.md");
320
+ const summaryText = doc ? getBriefSummary(doc.content) : t("missingBriefExplain");
321
+ return `<article class="brief-card ${compact ? "compact" : ""}">
322
+ <div class="card-head">
323
+ <div>
324
+ <a href="#/tasks/${encodeURIComponent(task.id)}">${escapeHtml(task.title)}</a>
325
+ <p>${escapeHtml(task.id)}</p>
326
+ </div>
327
+ ${tag(task.state)}
328
+ </div>
329
+ ${progressBar(task.completion)}
330
+ <div class="brief-content">
331
+ <p class="brief-teaser">${escapeHtml(summaryText)}</p>
332
+ </div>
333
+ <div class="card-actions">
334
+ ${taskCopyButton(task)}
335
+ <button class="btn-drawer-trigger" data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
336
+ </div>
337
+ </article>`;
338
+ }
339
+
340
+ function getBriefSummary(content) {
341
+ if (!content) return "";
342
+ let text = content
343
+ .replace(/#+\s+/g, "")
344
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
345
+ .replace(/[*_`]/g, "")
346
+ .replace(/-\s+/g, "")
347
+ .replace(/>\s+/g, "")
348
+ .replaceAll("\n", " ")
349
+ .replace(/\s+/g, " ")
350
+ .trim();
351
+ if (text.length > 140) text = text.slice(0, 137) + "...";
352
+ return text;
353
+ }
354
+
355
+ function generatedBrief(task) {
356
+ const phaseText = (task.phases || []).slice(0, 6).map((phase) => `<li><strong>${escapeHtml(phase.id)}</strong> ${escapeHtml(phase.output || phase.state)} · ${phase.completion}%</li>`).join("");
357
+ return `<div class="missing-brief">
358
+ <strong>${t("visibilityBriefMissing")}</strong>
359
+ <p>${t("missingBriefExplain")}</p>
360
+ <ul>${phaseText || `<li>${t("noPhaseData")}</li>`}</ul>
361
+ </div>`;
362
+ }
363
+
364
+ function clampCompletion(value) {
365
+ const number = Number(value) || 0;
366
+ return Math.max(0, Math.min(100, Math.round(number)));
367
+ }
368
+
369
+ function stateToColorVar(state) {
370
+ const map = { in_progress: "--accent", review: "--accent-2", blocked: "--danger", done: "--ok", planned: "--muted", not_started: "--muted" };
371
+ return map[state] || "--muted";
372
+ }
373
+
374
+ function taskSortLabel() {
375
+ return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
376
+ }
377
+
378
+ function taskDateKey(task) {
379
+ const source = `${task.shortId || ""} ${task.id || ""}`.trim();
380
+ const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
381
+ if (!match) return null;
382
+ const year = Number(match[1]);
383
+ const month = Number(match[2]);
384
+ const day = Number(match[3] || "1");
385
+ if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
386
+ return Date.UTC(year, month - 1, day);
387
+ }
388
+
389
+ function stableTaskLabel(task) {
390
+ return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
391
+ }
392
+
393
+ function compareTasksByTime(left, right) {
394
+ const leftDate = taskDateKey(left);
395
+ const rightDate = taskDateKey(right);
396
+ if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
397
+ return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
398
+ }
399
+ if (leftDate !== null && rightDate === null) return -1;
400
+ if (leftDate === null && rightDate !== null) return 1;
401
+ return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
402
+ }
403
+
404
+ function sortTasksByTime(tasks) {
405
+ return [...tasks].sort(compareTasksByTime);
406
+ }
407
+
408
+ function taskFolderName(task) {
409
+ const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
410
+ const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
411
+ return task?.shortId || fromPath || fromId || task?.title || "";
412
+ }
413
+
414
+ function taskCopyButton(task, extraClass = "") {
415
+ const folderName = taskFolderName(task);
416
+ return `<button type="button" class="copy-task-name ${extraClass}" data-copy-task-name="${escapeAttr(folderName)}" data-copy-task-folder="${escapeAttr(folderName)}" aria-label="${escapeAttr(t("copyTaskName"))}" title="${escapeAttr(t("copyTaskName"))}">
417
+ ${t("copyTaskNameShort")}
418
+ </button>`;
419
+ }
420
+
421
+ function taskGroupTimeKey(group) {
422
+ const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
423
+ if (!match) return null;
424
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
425
+ }
426
+
427
+ function taskToolbarCard(filteredCount) {
428
+ return `<section class="sidebar-card">
429
+ <h3>${t("filterTitle")}</h3>
430
+ <div class="input-group">
431
+ <input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
432
+ </div>
433
+ <div class="select-group">
434
+ <label>${t("stateFilter")}</label>
435
+ <select data-state-filter aria-label="${t("stateFilter")}">
436
+ ${["all", "in_progress", "review", "blocked", "planned", "done", "unknown"].map((value) => `<option value="${value}" ${state.taskState === value ? "selected" : ""}>${label(value)}</option>`).join("")}
437
+ </select>
438
+ </div>
439
+ <div class="select-group">
440
+ <label>${t("groupBy")}</label>
441
+ <select data-group-mode aria-label="${t("groupBy")}">
442
+ ${["migration", "module", "month", "state"].map((value) => `<option value="${value}" ${state.taskGroupMode === value ? "selected" : ""}>${t(`group_${value}`)}</option>`).join("")}
443
+ </select>
444
+ </div>
445
+ <div class="select-group">
446
+ <label>${t("layout")}</label>
447
+ <div class="layout-toggle-group">
448
+ <button class="layout-btn ${state.taskLayout === "list" ? "active" : ""}" data-layout="list" aria-label="${t("layoutList")}">
449
+ <svg style="width:12px;height:12px;vertical-align:middle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
450
+ ${t("layoutList")}
451
+ </button>
452
+ <button class="layout-btn ${state.taskLayout === "grid" ? "active" : ""}" data-layout="grid" aria-label="${t("layoutGrid")}">
453
+ <svg style="width:12px;height:12px;vertical-align:middle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
454
+ ${t("layoutGrid")}
455
+ </button>
456
+ </div>
457
+ </div>
458
+ <div class="select-group">
459
+ <label>${t("sortByTime")}</label>
460
+ <div class="layout-toggle-group sort-toggle-group">
461
+ <button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
462
+ ${t("sortNewest")}
463
+ </button>
464
+ <button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
465
+ ${t("sortOldest")}
466
+ </button>
467
+ </div>
468
+ </div>
469
+ <div class="search-stats">
470
+ ${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
471
+ </div>
147
472
  </section>`;
148
473
  }
149
474
 
150
- function taskTable() {
151
- const rows = bundle.status?.tasks || [];
152
- return tablePanel(t("tasks"), [t("task"), t("state"), t("completion"), t("evidence"), t("roadmapSource")], rows.map((task) => [
153
- clickable(task.title, "task", task.id),
154
- tag(task.state, label(task.state)),
155
- progress(task.completion),
156
- progress(evidenceHealth([task])),
157
- escapeHtml(label(task.roadmapSource || "unknown")),
158
- ]));
159
- }
160
-
161
- function ledgerSummary() {
162
- const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "harness-ledger");
163
- if (tables.length === 0) return "";
164
- const rows = tables.flatMap((table) => table.rows).slice(0, 5);
165
- return `<section class="panel ledger-summary">
166
- <div class="panel-head"><h2>${t("ledger")}</h2><span class="muted">${rows.length}</span></div>
167
- <div class="risk-list">${rows.map((row) => {
168
- const cells = row.cells || {};
169
- const title = cells.Task || cells.ID || cells.Item || cells.Title || cells.Module || "Ledger item";
170
- const state = cells.State || cells.Status || cells.Review || cells["Review State"] || "";
171
- const action = cells["Next Action"] || cells.Action || cells.Owner || cells["Required Action"] || "";
172
- return `<div class="risk-item"><strong>${escapeHtml(title)}</strong><p class="muted">${escapeHtml([label(state), action].filter(Boolean).join(" · "))}</p></div>`;
173
- }).join("")}</div>
475
+ function taskStatsCard() {
476
+ const allTasks = bundle.status?.tasks || [];
477
+ const avgCompletion = allTasks.length ? clampCompletion(allTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / allTasks.length) : 0;
478
+ return `<section class="sidebar-card">
479
+ <h3>${t("releaseHealth")}</h3>
480
+ <div class="stats-hero-gauge">
481
+ <span class="gauge-percentage">${avgCompletion}%</span>
482
+ <span class="gauge-label">${t("statOverall")}</span>
483
+ </div>
484
+ <div class="stats-breakdown">
485
+ ${[
486
+ { state: "in_progress", label: t("statInProgress"), colorVar: "--accent" },
487
+ { state: "review", label: t("statReview"), colorVar: "--accent-2" },
488
+ { state: "blocked", label: t("statBlocked"), colorVar: "--danger" },
489
+ { state: "done", label: t("statDone"), colorVar: "--ok" }
490
+ ].map(({ state, label, colorVar }) => {
491
+ const count = allTasks.filter(t => t.state === state).length;
492
+ return `<div class="stats-breakdown-row">
493
+ <span class="stat-label">
494
+ <span class="state-dot" style="background:var(${colorVar})"></span>
495
+ ${label}
496
+ </span>
497
+ <span class="stat-value">${count}</span>
498
+ </div>`;
499
+ }).join("")}
500
+ </div>
174
501
  </section>`;
175
502
  }
176
503
 
177
- function ledgerTable() {
178
- const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "harness-ledger");
179
- if (tables.length === 0) return emptyTable(t("ledger"), "No Harness Ledger found.");
180
- return genericTables(t("ledger"), tables);
181
- }
182
-
183
- function moduleTable() {
184
- const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "module-registry");
185
- const graph = graphPanel();
186
- if (tables.length === 0) return `${graph}${emptyTable(t("modules"), "No Module Registry found.")}`;
187
- return `${graph}${genericTables(t("modules"), tables)}`;
188
- }
189
-
190
- function evidenceTable() {
191
- const items = evidenceItems();
192
- return tablePanel(t("evidence"), [t("origin"), t("task"), t("state"), t("title"), t("affected")], items.map((item) => [
193
- escapeHtml(label(item.source)),
194
- escapeHtml(item.task || "-"),
195
- tag(item.state || "present", label(item.state || "present")),
196
- escapeHtml(item.title),
197
- escapeHtml(item.affected || ""),
198
- ]));
199
- }
200
-
201
- function evidenceItems() {
202
- const taskEvidence = (bundle.status?.tasks || []).flatMap((task) => [
203
- ...(task.evidence || []).map((item) => ({
204
- source: item.type || "task-progress",
205
- task: task.title,
206
- state: item.status || "present",
207
- title: item.summary || item.id,
208
- affected: item.path || "",
209
- })),
210
- ...(task.risks || []).map((item) => ({
211
- source: "task-review",
212
- task: task.title,
213
- state: item.open ? "open" : "closed",
214
- title: item.summary || item.id,
215
- affected: `${item.severity || ""}${item.blocksRelease ? " · blocks" : ""}`.trim(),
216
- })),
217
- ]);
218
- const qaTables = (bundle.tables?.tables || [])
219
- .filter((table) => ["task-review", "regression-ssot", "cadence-ledger"].includes(table.kind))
220
- .flatMap((table) => table.rows.slice(0, 12).map((row) => {
221
- const cells = row.cells || {};
222
- return {
223
- source: table.kind,
224
- task: cells.Task || cells.Module || cells.ID || "-",
225
- state: cells.Status || cells.State || cells.Open || cells.Verdict || "present",
226
- title: cells.Finding || cells.Summary || cells.Check || cells.Item || cells.Title || table.source,
227
- affected: cells["Evidence Checked"] || cells.Path || cells.Owner || cells["Required Action"] || table.source,
228
- };
229
- }));
230
- return [...taskEvidence, ...qaTables];
504
+ function taskLegendCard() {
505
+ return `<section class="sidebar-card">
506
+ <h3>${t("legendTitle")}</h3>
507
+ <div class="legend-list">
508
+ <div class="legend-item">
509
+ <span class="badge brief ready" style="margin-top:2px">
510
+ <svg style="width:10px;height:10px;margin-right:2px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
511
+ ${t("badgeBrief")}
512
+ </span>
513
+ <span>${t("legendBriefDesc")}</span>
514
+ </div>
515
+ <div class="legend-item">
516
+ <span class="badge map ready" style="margin-top:2px">
517
+ <svg style="width:10px;height:10px;margin-right:2px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
518
+ ${t("badgeMap")}
519
+ </span>
520
+ <span>${t("legendMapDesc")}</span>
521
+ </div>
522
+ </div>
523
+ </section>`;
524
+ }
525
+
526
+ function taskStatsBar() {
527
+ const allTasks = bundle.status?.tasks || [];
528
+ const inProgress = allTasks.filter(t => t.state === "in_progress").length;
529
+ const blocked = allTasks.filter(t => t.state === "blocked").length;
530
+ const done = allTasks.filter(t => t.state === "done").length;
531
+ const review = allTasks.filter(t => t.state === "review").length;
532
+ const avgCompletion = allTasks.length ? clampCompletion(allTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / allTasks.length) : 0;
533
+
534
+ return `<section class="task-stats-bar">
535
+ <div class="stat-chip">
536
+ <span class="stat-value">${allTasks.length}</span>
537
+ <span class="stat-label">${t("statTotal")}</span>
538
+ </div>
539
+ <div class="stat-chip in-progress">
540
+ <span class="stat-value">${inProgress}</span>
541
+ <span class="stat-label">${t("statInProgress")}</span>
542
+ </div>
543
+ <div class="stat-chip review">
544
+ <span class="stat-value">${review}</span>
545
+ <span class="stat-label">${t("statReview")}</span>
546
+ </div>
547
+ <div class="stat-chip blocked">
548
+ <span class="stat-value">${blocked}</span>
549
+ <span class="stat-label">${t("statBlocked")}</span>
550
+ </div>
551
+ <div class="stat-chip done">
552
+ <span class="stat-value">${done}</span>
553
+ <span class="stat-label">${t("statDone")}</span>
554
+ </div>
555
+ <div class="stat-chip completion">
556
+ <div class="stat-bar-track"><div class="stat-bar-fill" style="width:${avgCompletion}%"></div></div>
557
+ <div style="text-align:right">
558
+ <span class="stat-value">${avgCompletion}%</span>
559
+ <span class="stat-label" style="display:block;margin-top:2px">${t("statOverall")}</span>
560
+ </div>
561
+ </div>
562
+ </section>`;
231
563
  }
232
564
 
233
- function lessonsTable() {
234
- const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "lessons-ssot");
235
- if (tables.length === 0) return emptyTable(t("lessons"), "No Lessons SSoT found.");
236
- return genericTables(t("lessons"), tables);
565
+ function taskRow(task) {
566
+ const completion = clampCompletion(task.completion);
567
+ const briefReady = task.briefSource === "standalone" || !!taskDocument(task, "brief.md");
568
+ const mapReady = !!taskDocument(task, "visual_map.md");
569
+ const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
570
+ const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
571
+
572
+ return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
573
+ <div class="row-accent-bar"></div>
574
+ <div class="row-main">
575
+ <strong>${escapeHtml(task.title)}</strong>
576
+ <span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
577
+ ${taskCopyButton(task, "row-copy")}
578
+ </div>
579
+ <div class="row-status">${tag(task.state)}</div>
580
+ <div class="row-progress">
581
+ <div class="mini-progress-track"><div class="mini-progress-fill" style="width:${completion}%"></div></div>
582
+ <span class="row-pct">${completion}%</span>
583
+ </div>
584
+ <div class="row-brief ${briefReady ? "ready" : "missing"}" title="${escapeAttr(briefLabel)}" aria-label="${escapeAttr(briefLabel)}">
585
+ <span class="badge brief ${briefReady ? "ready" : "missing"}">
586
+ <svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
587
+ ${briefReady ? t("badgeBrief") : t("badgeBriefMissing")}
588
+ </span>
589
+ </div>
590
+ <div class="row-map ${mapReady ? "ready" : "missing"}" title="${escapeAttr(mapLabel)}" aria-label="${escapeAttr(mapLabel)}">
591
+ <span class="badge map ${mapReady ? "ready" : "missing"}">
592
+ <svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
593
+ ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
594
+ </span>
595
+ </div>
596
+ </article>`;
237
597
  }
238
598
 
239
- function adoption() {
240
- const advice = bundle.adoption?.warnings || [];
241
- return `<section class="page-grid">
242
- <div>
243
- ${tablePanel(t("adoption"), [t("category"), t("severity"), t("title"), t("affected"), t("requiredAction")], advice.map((item) => [
244
- escapeHtml(item.category),
245
- tag(item.severity, label(item.severity)),
246
- escapeHtml(item.title),
247
- escapeHtml(item.affected),
248
- escapeHtml(item.requiredAction),
249
- ]))}
250
- <section class="panel" style="padding:16px;margin-top:16px">
251
- <h2>${t("nextAction")}</h2>
252
- <ol>${((bundle.adoption?.manualSteps || {})[state.lang] || []).map((step) => `<li>${escapeHtml(step)}</li>`).join("")}</ol>
599
+ function taskIndex() {
600
+ const tasks = filteredTasks();
601
+ const groups = taskGroups(tasks);
602
+ const orderedGroups = orderedTaskGroups(groups);
603
+ const groupPageCount = Math.max(1, Math.ceil(orderedGroups.length / taskGroupsPerPage));
604
+ const groupPage = Math.min(Math.max(1, Number(state.taskGroupPage) || 1), groupPageCount);
605
+ const visibleGroups = orderedGroups.slice((groupPage - 1) * taskGroupsPerPage, groupPage * taskGroupsPerPage);
606
+
607
+ return `<div class="tasks-grid">
608
+ <div class="tasks-main stack">
609
+ ${taskStatsBar()}
610
+ ${visibleGroups.map(([group, groupTasks]) => taskGroup(group, groupTasks)).join("")}
611
+ <section class="group-pager">
612
+ <span>${t("showingGroups")} ${visibleGroups.length ? (groupPage - 1) * taskGroupsPerPage + 1 : 0}-${Math.min(groupPage * taskGroupsPerPage, orderedGroups.length)} / ${orderedGroups.length}</span>
613
+ ${pager("task-groups", groupPage, groupPageCount)}
253
614
  </section>
254
615
  </div>
255
- ${drawer()}
616
+ <aside class="tasks-sidebar stack">
617
+ ${taskToolbarCard(tasks.length)}
618
+ ${taskStatsCard()}
619
+ ${taskLegendCard()}
620
+ </aside>
621
+ </div>`;
622
+ }
623
+
624
+ function orderedTaskGroups(groups) {
625
+ const rank = (group) => {
626
+ if (group.startsWith("module:")) return 2;
627
+ if (group.startsWith("state:")) return 2;
628
+ if (group.startsWith("month:")) return 2;
629
+ if (group === "active") return 0;
630
+ if (group === "brief-ready") return 1;
631
+ if (group.startsWith("legacy:")) return 2;
632
+ if (group === "unknown") return 3;
633
+ return 4;
634
+ };
635
+ return Object.entries(groups).sort(([left], [right]) => {
636
+ const rankDiff = rank(left) - rank(right);
637
+ if (rankDiff !== 0) return rankDiff;
638
+ const leftTime = taskGroupTimeKey(left);
639
+ const rightTime = taskGroupTimeKey(right);
640
+ if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
641
+ return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
642
+ }
643
+ if (leftTime !== null && rightTime === null) return -1;
644
+ if (leftTime === null && rightTime !== null) return 1;
645
+ return left.localeCompare(right);
646
+ });
647
+ }
648
+
649
+ function taskGroups(tasks) {
650
+ if (state.taskGroupMode === "module") {
651
+ return groupBy(tasks, (task) => `module:${taskModuleKey(task)}`);
652
+ }
653
+ if (state.taskGroupMode === "month") {
654
+ return groupBy(tasks, (task) => {
655
+ const match = task.shortId?.match(/^(\d{4}-\d{2})/);
656
+ return match ? `month:${match[1]}` : "month:unknown";
657
+ });
658
+ }
659
+ if (state.taskGroupMode === "state") {
660
+ return groupBy(tasks, (task) => `state:${task.state || "unknown"}`);
661
+ }
662
+ return groupBy(tasks, (task) => {
663
+ if (["in_progress", "review", "blocked", "planned", "not_started"].includes(task.state)) return "active";
664
+ if (task.briefSource === "standalone") return "brief-ready";
665
+ const match = task.shortId?.match(/^(\d{4}-\d{2})/);
666
+ return match ? `legacy:${match[1]}` : task.state || "unknown";
667
+ });
668
+ }
669
+
670
+ function taskGroup(group, tasks) {
671
+ const orderedTasks = sortTasksByTime(tasks);
672
+ const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
673
+ const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
674
+ const start = (page - 1) * taskPageSize;
675
+ const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
676
+ const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
677
+
678
+ const isGrid = state.taskLayout === "grid";
679
+ const layoutClass = isGrid ? "task-card-grid" : "task-list";
680
+ const itemRenderer = isGrid ? taskCard : taskRow;
681
+ const listHeader = isGrid ? "" : `<div class="task-list-header">
682
+ <div class="col-main">${t("columnTask")}</div>
683
+ <div class="col-status">${t("columnState")}</div>
684
+ <div class="col-progress">${t("columnCompletion")}</div>
685
+ <div class="col-brief">${t("columnBrief")}</div>
686
+ <div class="col-map">${t("badgeMap")}</div>
687
+ </div>`;
688
+
689
+ return `<section class="task-group">
690
+ <div class="section-head">
691
+ <div>
692
+ <h2>${taskGroupLabel(group)}</h2>
693
+ <p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
694
+ </div>
695
+ <div class="group-actions">
696
+ <div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
697
+ <div class="group-progress-track"><div class="group-progress-fill" style="width:${avgCompletion}%"></div></div>
698
+ <span>${avgCompletion}%</span>
699
+ </div>
700
+ ${pager("task", page, pageCount, group)}
701
+ </div>
702
+ </div>
703
+ <div class="${layoutClass}">
704
+ ${listHeader}
705
+ ${visibleTasks.map(itemRenderer).join("")}
706
+ </div>
707
+ </section>`;
708
+ }
709
+
710
+ function taskCard(task) {
711
+ const completion = clampCompletion(task.completion);
712
+ const stateColor = stateToColorVar(task.state);
713
+ const briefReady = task.briefSource === "standalone" || !!taskDocument(task, "brief.md");
714
+ const mapReady = !!taskDocument(task, "visual_map.md");
715
+ const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
716
+ const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
717
+
718
+ return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
719
+ <div class="card-header">
720
+ <span class="card-id">${escapeHtml(task.id)}</span>
721
+ <div class="card-header-actions">
722
+ ${taskCopyButton(task, "compact")}
723
+ ${tag(task.state)}
724
+ </div>
725
+ </div>
726
+ <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
727
+ <div class="card-meta">
728
+ <span class="meta-module" title="${escapeAttr(taskModuleKey(task))}">
729
+ <svg style="width:12px;height:12px;vertical-align:middle;margin-right:2px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
730
+ ${escapeHtml(taskModuleKey(task))}
731
+ </span>
732
+ </div>
733
+ <div class="card-progress">
734
+ <div class="card-progress-track"><div class="card-progress-fill" style="width:${completion}%"></div></div>
735
+ <span class="progress-pct">${completion}%</span>
736
+ </div>
737
+ <div class="card-badges">
738
+ <span class="badge brief ${briefReady ? "ready" : "missing"}" title="${escapeAttr(briefLabel)}">
739
+ <svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
740
+ ${briefReady ? t("badgeBrief") : t("badgeBriefMissing")}
741
+ </span>
742
+ <span class="badge map ${mapReady ? "ready" : "missing"}" title="${escapeAttr(mapLabel)}">
743
+ <svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
744
+ ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
745
+ </span>
746
+ </div>
747
+ </article>`;
748
+ }
749
+
750
+ function taskGroupLabel(group) {
751
+ if (group === "active") return t("activeCurrent");
752
+ if (group === "brief-ready") return t("briefReadyGroup");
753
+ if (group.startsWith("legacy:")) return `${t("legacyMonth")} ${group.slice("legacy:".length)}`;
754
+ if (group.startsWith("module:")) return `${t("inferredModule")} · ${group.slice("module:".length)}`;
755
+ if (group.startsWith("month:")) return `${t("legacyMonth")} ${group.slice("month:".length)}`;
756
+ if (group.startsWith("state:")) return `${t("columnState")} · ${label(group.slice("state:".length))}`;
757
+ return label(group);
758
+ }
759
+
760
+ function filteredTasks() {
761
+ const query = state.query.trim().toLowerCase();
762
+ return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
763
+ const stateMatch = state.taskState === "all" || task.state === state.taskState;
764
+ if (!stateMatch) return false;
765
+ if (!query) return true;
766
+ return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
767
+ }));
768
+ }
769
+
770
+ function taskModuleKey(task) {
771
+ return task.module || task.inferredModule || "legacy-unclassified";
772
+ }
773
+
774
+ function taskDetail(route) {
775
+ const taskId = route.id;
776
+ const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
777
+ if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
778
+ return `<main class="task-detail">
779
+ <nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
780
+ <section class="detail-hero">
781
+ <div>
782
+ <p class="eyebrow">${t("taskVisibility")}</p>
783
+ <h2>${escapeHtml(task.title)}</h2>
784
+ <p>${escapeHtml(task.path)}</p>
785
+ ${taskCopyButton(task, "detail-copy")}
786
+ </div>
787
+ <div class="detail-score">${task.completion}%</div>
788
+ </section>
789
+ ${taskStateSummary(task)}
790
+ ${phaseTimeline(task)}
791
+ <section class="detail-grid">
792
+ <article class="detail-main">
793
+ ${taskDocumentLibrary(task, route.doc)}
794
+ </article>
795
+ <aside class="detail-side">
796
+ ${reviewActionPanel(task, { mode: "summary" })}
797
+ ${lessonCandidatePanel(task, { context: "detail" })}
798
+ ${openFindings(task)}
799
+ ${evidenceList(task)}
800
+ ${documentTabs(task)}
801
+ </aside>
802
+ </section>
803
+ </main>`;
804
+ }
805
+
806
+ function taskStateSummary(task) {
807
+ return `<section class="task-state-summary">
808
+ <div>
809
+ <span>${t("legacyState")}</span>
810
+ ${tag(task.state)}
811
+ </div>
812
+ <div>
813
+ <span>${t("lifecycleState")}</span>
814
+ ${tag(task.lifecycleState || "unknown")}
815
+ </div>
816
+ <div>
817
+ <span>${t("reviewStatus")}</span>
818
+ ${tag(task.reviewStatus || "missing")}
819
+ </div>
820
+ <div>
821
+ <span>${t("sedimentationStatus")}</span>
822
+ ${tag(task.lessonCandidateStatus || "missing")}
823
+ </div>
824
+ <div>
825
+ <span>${t("closeoutStatus")}</span>
826
+ ${tag(task.closeoutStatus || "missing")}
827
+ </div>
828
+ <div>
829
+ <span>${t("lifecycleQueues")}</span>
830
+ ${(task.taskQueues || []).map(tag).join("") || tag("active")}
831
+ </div>
832
+ ${taskQueueReasonSummary(task)}
833
+ </section>`;
834
+ }
835
+
836
+ function taskQueueReasonSummary(task) {
837
+ const reasons = task.queueReasons || [];
838
+ if (!reasons.length) return "";
839
+ return `<div class="task-queue-reasons">
840
+ <span>${t("queueReasons")}</span>
841
+ <div class="review-reasons">
842
+ ${reasons.slice(0, 5).map(reviewReason).join("")}
843
+ </div>
844
+ </div>`;
845
+ }
846
+
847
+ function phaseTimeline(task) {
848
+ return `<section class="phase-timeline">
849
+ <h2>${t("phaseTimeline")}</h2>
850
+ ${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
851
+ <strong>${escapeHtml(phase.id)}</strong>
852
+ <span>${phase.completion}%</span>
853
+ <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
854
+ ${progressBar(phase.completion)}
855
+ </div>`).join("") || emptyState(t("noPhaseData"))}
856
+ </section>`;
857
+ }
858
+
859
+ function taskDocSection(task, fileName, title, required) {
860
+ const doc = taskDocument(task, fileName);
861
+ if (!doc && !required) return "";
862
+ return `<section class="doc-section">
863
+ <div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
864
+ <div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
865
+ </section>`;
866
+ }
867
+
868
+ function taskDocumentLibrary(task, selectedTab) {
869
+ const docs = orderedTaskDocuments(task);
870
+ if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
871
+ const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
872
+ return `<section class="doc-library">
873
+ <div class="section-head">
874
+ <div>
875
+ <p class="eyebrow">${t("taskDocuments")}</p>
876
+ <h2>${escapeHtml(t("sourceDocuments"))}</h2>
877
+ </div>
878
+ <button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
879
+ </div>
880
+ <div class="doc-accordion-list">
881
+ ${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
882
+ </div>
883
+ </section>`;
884
+ }
885
+
886
+ function orderedTaskDocuments(task) {
887
+ const docs = taskDocTabs
888
+ .map(([key, file]) => {
889
+ const doc = taskDocument(task, file);
890
+ if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
891
+ if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
892
+ return null;
893
+ })
894
+ .filter(Boolean);
895
+ const priority = taskDocumentPriority(task);
896
+ const rank = new Map(priority.map((key, index) => [key, index]));
897
+ return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
898
+ }
899
+
900
+ function taskDocumentPriority(task) {
901
+ const stateName = task?.state || "";
902
+ const lifecycle = task?.lifecycleState || "";
903
+ if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
904
+ return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
905
+ }
906
+ if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
907
+ return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
908
+ }
909
+ if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
910
+ return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
911
+ }
912
+ return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
913
+ }
914
+
915
+ function defaultTaskDocumentKey(task, docs) {
916
+ const priority = taskDocumentPriority(task);
917
+ return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
918
+ }
919
+
920
+ function documentAccordion(item, open) {
921
+ return `<details class="doc-accordion" ${open ? "open" : ""}>
922
+ <summary>
923
+ <span>${escapeHtml(item.title)}</span>
924
+ <small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
925
+ </summary>
926
+ <div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
927
+ </details>`;
928
+ }
929
+
930
+ function documentTabs(task) {
931
+ const docs = orderedTaskDocuments(task);
932
+ return `<section class="side-panel">
933
+ <h3>${t("sourceDocuments")}</h3>
934
+ ${docs.map((doc) => `<a href="#/tasks/${encodeURIComponent(task.id)}/docs/${encodeURIComponent(doc.key)}" title="${escapeAttr(doc.path)}">${escapeHtml(doc.title)}</a>`).join("") || `<p>${t("noDocuments")}</p>`}
256
935
  </section>`;
257
936
  }
258
937
 
259
- function settings() {
260
- return `<section class="panel" style="padding:18px">
261
- <h2>${t("settings")}</h2>
262
- <p class="muted">These settings are browser-local and never write back to the project.</p>
263
- <p>${t("rendered")} / ${t("source")}: Switch inside the detail drawer.</p>
938
+ function selectedSourceDocument(task, tab) {
939
+ if (!tab) return "";
940
+ const match = taskDocTabs.find(([key]) => key === tab);
941
+ if (!match) return "";
942
+ const doc = taskDocument(task, match[1]);
943
+ if (!doc) return "";
944
+ return `<section class="doc-section selected-source">
945
+ <div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
946
+ <div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
264
947
  </section>`;
265
948
  }
266
949
 
267
- function withDrawer(content) {
268
- return `<section class="page-grid"><div>${content}</div>${drawer()}</section>`;
950
+ function openFindings(task) {
951
+ const risks = task.risks || [];
952
+ return `<section class="side-panel">
953
+ <h3>${t("openFindings")}</h3>
954
+ ${risks.map((risk) => `<div class="finding ${risk.open || risk.blocksRelease ? "open" : ""}"><strong>${escapeHtml(risk.severity)}</strong><span>${escapeHtml(risk.summary)}</span></div>`).join("") || `<p>${t("noOpenFindings")}</p>`}
955
+ </section>`;
269
956
  }
270
957
 
271
- function tablePanel(title, headers, rows) {
272
- return `<section class="table-panel">
273
- <div class="panel-head"><h2>${escapeHtml(title)}</h2><span class="muted">${rows.length}</span></div>
274
- <div class="table-wrap"><table>
275
- <thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead>
276
- <tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody>
277
- </table></div>
958
+ function reviewActionPanel(task, { mode = "summary" } = {}) {
959
+ if (!isTaskInReviewQueue(task)) return "";
960
+ const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
961
+ const confirmed = task.reviewStatus === "confirmed";
962
+ const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
963
+ const candidateStatus = task.lessonCandidateStatus || "missing";
964
+ if (mode !== "workspace") {
965
+ return `<section class="side-panel review-actions">
966
+ <h3>${t("reviewActions")}</h3>
967
+ <p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
968
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
969
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
970
+ </section>`;
971
+ }
972
+ if (!canUseWorkbenchAction("review-complete")) {
973
+ return `<section class="side-panel review-actions">
974
+ <h3>${t("reviewActions")}</h3>
975
+ <p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
976
+ </section>`;
977
+ }
978
+ if (confirmed) {
979
+ return `<section class="side-panel review-actions">
980
+ <h3>${t("reviewActions")}</h3>
981
+ <p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
982
+ </section>`;
983
+ }
984
+ const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
985
+ const queueBlocked = !taskCanBeHumanConfirmed(task);
986
+ const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
987
+ const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
988
+ return `<section class="side-panel review-actions">
989
+ <h3>${t("reviewActions")}</h3>
990
+ <p>${escapeHtml(message)}</p>
991
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
992
+ <label class="review-check">
993
+ <input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
994
+ <span>${t("reviewConfirmChecklist")}</span>
995
+ </label>
996
+ <div class="review-confirm-copy">
997
+ ${taskCopyButton(task, "review-copy-task-name")}
998
+ </div>
999
+ <input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
1000
+ <button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
1001
+ <div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
278
1002
  </section>`;
279
1003
  }
280
1004
 
281
- function genericTables(title, tables) {
282
- return tables.map((table) => tablePanel(`${title} · ${table.source}`, table.columns, table.rows.map((row) => table.columns.map((column) => escapeHtml(row.cells[column] || ""))))).join("");
1005
+ function isTaskInReviewQueue(task) {
1006
+ return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
283
1007
  }
284
1008
 
285
- function graphPanel() {
1009
+ function taskCanBeHumanConfirmed(task) {
1010
+ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
1011
+ }
1012
+
1013
+ function evidenceList(task) {
1014
+ const evidence = task.evidence || [];
1015
+ return `<section class="side-panel">
1016
+ <h3>${t("evidence")}</h3>
1017
+ ${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
1018
+ </section>`;
1019
+ }
1020
+
1021
+ function modulesView(moduleId = "") {
286
1022
  const graph = bundle.graph || { nodes: [], edges: [] };
287
- const modules = graph.nodes.filter((node) => node.type === "module");
288
- const fallbackTasks = graph.nodes.filter((node) => node.type === "task");
289
- const laneNodes = modules.length > 0 ? modules : fallbackTasks;
290
- const edges = graph.edges || [];
291
- return `<section class="panel graph-panel">
292
- <div class="panel-head"><h2>${t("graph")}</h2><span class="muted">${modules.length} modules · ${fallbackTasks.length} tasks · ${edges.length} edges</span></div>
293
- <div class="graph-lanes">${laneNodes.slice(0, 12).map((module) => {
294
- const owned = edges.filter((edge) => edge.from === module.id).slice(0, 8);
295
- return `<div class="lane"><strong>${escapeHtml(module.label)}</strong><span>${label(module.state || "")}</span>${owned.map((edge) => `<small>${escapeHtml(edge.type)} → ${escapeHtml(edge.to.replace(/^step:/, ""))}</small>`).join("")}</div>`;
296
- }).join("") || `<p class="empty">No module graph data</p>`}</div>
1023
+ const explicitModules = (graph.nodes || []).filter((node) => node.type === "module");
1024
+ const moduleMap = new Map(explicitModules.map((module) => [module.id.replace(/^module:/, ""), module]));
1025
+ for (const task of bundle.status?.tasks || []) {
1026
+ const key = taskModuleKey(task);
1027
+ if (!moduleMap.has(key)) moduleMap.set(key, { id: `module:${key}`, type: "module", label: key, state: task.classificationSource || "inferred" });
1028
+ }
1029
+ const modules = [...moduleMap.values()];
1030
+ return `<main class="stack">
1031
+ <section class="module-grid">
1032
+ ${modules.map((module) => moduleCard(module)).join("") || emptyState(t("noModules"))}
1033
+ </section>
1034
+ </main>`;
1035
+ }
1036
+
1037
+ function moduleTaskRow(task) {
1038
+ const dotClass = /fail|blocked|open/i.test(task.state) ? "state-fail" : /warn|advice|planned|missing|unknown/i.test(task.state) ? "state-warn" : "state-pass";
1039
+ return `<a class="module-task-row" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}">
1040
+ <div class="module-task-left">
1041
+ <i class="module-task-dot ${dotClass}" title="${escapeAttr(task.state)}"></i>
1042
+ <span class="module-task-title">${escapeHtml(task.title)}</span>
1043
+ </div>
1044
+ <span class="module-task-pct">${task.completion}%</span>
1045
+ </a>`;
1046
+ }
1047
+
1048
+ function moduleCard(module) {
1049
+ const moduleKey = module.id.replace(/^module:/, "");
1050
+ const tasks = (bundle.status?.tasks || []).filter((task) => taskModuleKey(task) === moduleKey);
1051
+
1052
+ // Inline Pagination
1053
+ state.modulePages = state.modulePages || {};
1054
+ const currentPage = state.modulePages[moduleKey] || 1;
1055
+ const pageCount = Math.ceil(tasks.length / 8) || 1;
1056
+ const visibleTasks = tasks.slice((currentPage - 1) * 8, currentPage * 8);
1057
+
1058
+ const brief = findDocument(`TARGET:docs/09-PLANNING/MODULES/${moduleKey}/brief.md`);
1059
+
1060
+ let pagerHtml = "";
1061
+ if (tasks.length > 8) {
1062
+ pagerHtml = `<div class="module-pager">
1063
+ <button ${currentPage <= 1 ? "disabled" : ""} onclick="window.setModulePage('${escapeAttr(moduleKey)}', ${currentPage - 1})">${t("prevPage")}</button>
1064
+ <span>${currentPage} / ${pageCount}</span>
1065
+ <button ${currentPage >= pageCount ? "disabled" : ""} onclick="window.setModulePage('${escapeAttr(moduleKey)}', ${currentPage + 1})">${t("nextPage")}</button>
1066
+ </div>`;
1067
+ }
1068
+
1069
+ return `<article class="module-card">
1070
+ <div class="card-head"><h2>${escapeHtml(module.label || moduleKey)}</h2>${tag(module.state || "unknown")}</div>
1071
+ <div class="markdown">${brief ? window.HarnessMarkdown.render(brief.content, "rendered") : `<p>${t("moduleBriefMissing")}</p>`}</div>
1072
+ <h3>${t("moduleTasks")} · ${tasks.length}</h3>
1073
+ <div class="module-task-list">
1074
+ ${visibleTasks.map(moduleTaskRow).join("") || `<p>${t("noModuleTasks")}</p>`}
1075
+ </div>
1076
+ ${pagerHtml}
1077
+ </article>`;
1078
+ }
1079
+
1080
+ function reviewQueue() {
1081
+ ensureReviewQueueState();
1082
+ const tabs = reviewQueueTabs();
1083
+ const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
1084
+ const baseTasks = reviewQueueBaseTasks(activeTab);
1085
+ const reasonOptions = reviewReasonOptions(baseTasks);
1086
+ normalizeReviewReasonFilter(reasonOptions);
1087
+ const tasks = reviewFilteredTasks(baseTasks);
1088
+ const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
1089
+ const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
1090
+ const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
1091
+ return `<div class="dashboard-grid review-queue-page">
1092
+ <main class="dashboard-main stack">
1093
+ <section class="flow-panel">
1094
+ <div class="section-head">
1095
+ <div>
1096
+ <p class="eyebrow">${t("review")}</p>
1097
+ <h2>${t("reviewQueue")}</h2>
1098
+ <p class="subtle">${t("reviewQueueSubtitle")}</p>
1099
+ </div>
1100
+ <span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
1101
+ </div>
1102
+ <div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
1103
+ ${tabs.map((tab) => reviewQueueTab(tab)).join("")}
1104
+ </div>
1105
+ <div class="review-queue-toolbar">
1106
+ <div class="input-group">
1107
+ <input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
1108
+ </div>
1109
+ <div class="select-group">
1110
+ <label>${t("reasonFilter")}</label>
1111
+ <select data-review-reason-filter aria-label="${t("reasonFilter")}">
1112
+ <option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
1113
+ ${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
1114
+ </select>
1115
+ </div>
1116
+ <div class="select-group">
1117
+ <label>${t("sortBy")}</label>
1118
+ <select data-review-sort aria-label="${t("sortBy")}">
1119
+ ${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
1120
+ </select>
1121
+ </div>
1122
+ </div>
1123
+ <div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
1124
+ <div class="review-queue-list">
1125
+ ${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
1126
+ </div>
1127
+ </div>
1128
+ <div class="review-queue-pager">
1129
+ ${pager("review", page, pageCount)}
1130
+ </div>
1131
+ </section>
1132
+ </main>
1133
+ <aside class="dashboard-sidebar stack">
1134
+ <section class="side-panel review-queue-summary">
1135
+ <h3>${t("reviewQueue")}</h3>
1136
+ <div class="review-queue-stats">
1137
+ ${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
1138
+ </div>
1139
+ </section>
1140
+ <section class="side-panel">
1141
+ <h3>${escapeHtml(activeTab.label)}</h3>
1142
+ <p>${escapeHtml(activeTab.description)}</p>
1143
+ <dl class="review-queue-contract">
1144
+ <div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
1145
+ <div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
1146
+ </dl>
1147
+ </section>
1148
+ </aside>
1149
+ </div>`;
1150
+ }
1151
+
1152
+ function ensureReviewQueueState() {
1153
+ if (!state.reviewQueueTab) state.reviewQueueTab = "review";
1154
+ if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
1155
+ if (!state.reviewSort) state.reviewSort = "queue";
1156
+ if (!state.reviewQueuePage) state.reviewQueuePage = 1;
1157
+ }
1158
+
1159
+ function reviewQueueTabs() {
1160
+ return [
1161
+ { id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
1162
+ { id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
1163
+ { id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
1164
+ { id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
1165
+ { id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
1166
+ { id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
1167
+ ];
1168
+ }
1169
+
1170
+ function reviewQueueTab(tab) {
1171
+ const active = tab.id === state.reviewQueueTab;
1172
+ const count = reviewQueueBaseTasks(tab).length;
1173
+ return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
1174
+ <span>${escapeHtml(tab.label)}</span>
1175
+ <strong>${count}</strong>
1176
+ </button>`;
1177
+ }
1178
+
1179
+ function reviewSortOptions() {
1180
+ return [
1181
+ { id: "queue", label: t("sortQueuePriority") },
1182
+ { id: "newest", label: t("sortNewest") },
1183
+ { id: "oldest", label: t("sortOldest") },
1184
+ { id: "id", label: t("sortTaskId") },
1185
+ ];
1186
+ }
1187
+
1188
+ function reviewQueueBaseTasks(tab) {
1189
+ return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
1190
+ }
1191
+
1192
+ function taskMatchesReviewTab(task, tab) {
1193
+ const queues = reviewTaskQueues(task);
1194
+ return (tab.queues || []).some((queue) => queues.includes(queue));
1195
+ }
1196
+
1197
+ function reviewTaskQueues(task) {
1198
+ return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
1199
+ }
1200
+
1201
+ function reviewReasonOptions(tasks) {
1202
+ return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
1203
+ }
1204
+
1205
+ function normalizeReviewReasonFilter(reasonOptions) {
1206
+ const current = state.reviewReasonFilter || "all";
1207
+ if (current === "all") return;
1208
+ if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
1209
+ }
1210
+
1211
+ function reviewFilteredTasks(tasks) {
1212
+ const query = state.query.trim().toLowerCase();
1213
+ const reasonFilter = state.reviewReasonFilter || "all";
1214
+ return [...tasks]
1215
+ .filter((task) => {
1216
+ if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
1217
+ if (!query) return true;
1218
+ return [
1219
+ task.id,
1220
+ task.shortId,
1221
+ task.title,
1222
+ task.module,
1223
+ task.inferredModule,
1224
+ task.state,
1225
+ task.lifecycleState,
1226
+ task.reviewStatus,
1227
+ task.closeoutStatus,
1228
+ ...(task.taskQueues || []),
1229
+ ...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
1230
+ ].some((value) => String(value || "").toLowerCase().includes(query));
1231
+ })
1232
+ .sort(reviewTaskSort);
1233
+ }
1234
+
1235
+ function reviewTaskSort(left, right) {
1236
+ if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
1237
+ if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
1238
+ if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
1239
+ return reviewPriorityRank(left) - reviewPriorityRank(right)
1240
+ || compareTasksByTimeForOrder(left, right, "desc")
1241
+ || stableTaskLabel(left).localeCompare(stableTaskLabel(right));
1242
+ }
1243
+
1244
+ function compareTasksByTimeForOrder(left, right, order) {
1245
+ const previous = state.taskSortOrder;
1246
+ state.taskSortOrder = order;
1247
+ const result = compareTasksByTime(left, right);
1248
+ state.taskSortOrder = previous;
1249
+ return result;
1250
+ }
1251
+
1252
+ function reviewPriorityRank(task) {
1253
+ const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
1254
+ const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
1255
+ const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
1256
+ const queues = reviewTaskQueues(task);
1257
+ const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
1258
+ return Math.min(reasonRank, taskQueueRank);
1259
+ }
1260
+
1261
+ function reviewTruthyCount(tasks, key) {
1262
+ return tasks.filter((task) => task[key] === true).length;
1263
+ }
1264
+
1265
+ function reviewQueueCard(task, tab) {
1266
+ const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
1267
+ const reasons = task.queueReasons || [];
1268
+ const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
1269
+ const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
1270
+ const displayId = task.shortId || taskFolderName(task) || task.id;
1271
+ return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
1272
+ <div class="card-header">
1273
+ <span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
1274
+ ${tag(task.reviewStatus || "missing")}
1275
+ ${reviewTaskQueues(task).map(tag).join("")}
1276
+ </div>
1277
+ <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
1278
+ <div class="card-meta">
1279
+ <span>${tag(task.lifecycleState || "unknown")}</span>
1280
+ <span>${tag(task.closeoutStatus || "missing")}</span>
1281
+ <span>${openMaterial} ${t("openFindings")}</span>
1282
+ <span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
1283
+ <span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
1284
+ </div>
1285
+ <p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
1286
+ ${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
1287
+ ${lessonActions}
1288
+ <div class="review-queue-actions">
1289
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
1290
+ <a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
1291
+ <button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
1292
+ ${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
1293
+ </div>
1294
+ </article>`;
1295
+ }
1296
+
1297
+ function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
1298
+ const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
1299
+ if (!candidates.length) return "";
1300
+ const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
1301
+ const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
1302
+ const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
1303
+ return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
1304
+ <div class="lesson-candidate-panel-head">
1305
+ <div>
1306
+ <p class="eyebrow">${t("lessonCandidates")}</p>
1307
+ <h3>${t("lessonSedimentationActions")}</h3>
1308
+ </div>
1309
+ <span class="tag">${visibleCandidates.length}/${candidates.length}</span>
1310
+ </div>
1311
+ ${staticNote}
1312
+ <div class="lesson-candidate-actions">
1313
+ ${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
1314
+ </div>
1315
+ ${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
297
1316
  </section>`;
298
1317
  }
299
1318
 
300
- function emptyTable(title, message) {
301
- return `<section class="table-panel"><div class="panel-head"><h2>${escapeHtml(title)}</h2></div><p class="empty">${escapeHtml(message)}</p></section>`;
1319
+ function lessonCandidateAction(task, candidate) {
1320
+ const followUp = String(candidate.followUpTask || "").trim();
1321
+ const hasFollowUp = followUp && !/^pending$/i.test(followUp);
1322
+ const prompt = lessonSedimentationPrompt(task, candidate);
1323
+ return `<div class="lesson-candidate-action">
1324
+ <div class="lesson-candidate-main">
1325
+ <strong>${escapeHtml(candidate.id)}</strong>
1326
+ <span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
1327
+ <small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
1328
+ </div>
1329
+ <span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
1330
+ <div class="lesson-candidate-command-row">
1331
+ ${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
1332
+ <button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
1333
+ <button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
1334
+ </div>
1335
+ </div>`;
1336
+ }
1337
+
1338
+ function lessonSedimentationPrompt(task, candidate) {
1339
+ return [
1340
+ "You are executing a lesson sedimentation follow-up task.",
1341
+ "",
1342
+ `Source task: ${task.id}`,
1343
+ `Source candidate: ${candidate.id} - ${candidate.title || ""}`,
1344
+ `Candidate scope: ${candidate.scope || "unspecified"}`,
1345
+ `Candidate module key: ${candidate.moduleKey || "n/a"}`,
1346
+ `Detail artifact: ${candidate.detailArtifact || "not provided"}`,
1347
+ `Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
1348
+ `Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
1349
+ `Promotion target: ${candidate.promotionTarget || "unspecified"}`,
1350
+ `Conflict check: ${candidate.conflictCheck || "pending"}`,
1351
+ `Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
1352
+ "",
1353
+ "Instructions:",
1354
+ "1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
1355
+ "2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
1356
+ "3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
1357
+ "4. Check conflicts against existing lessons and standards.",
1358
+ "5. Propose the smallest diff first.",
1359
+ "6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
1360
+ ].join("\n");
302
1361
  }
303
1362
 
304
- function drawer() {
305
- const selection = state.selected;
306
- if (!selection) {
307
- return `<aside class="drawer"><div class="drawer-head"><h2>${t("detail")}</h2></div><div class="drawer-body empty">${t("noSelection")}</div></aside>`;
1363
+ function reviewReason(reason) {
1364
+ return `<div class="review-reason">
1365
+ <strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
1366
+ <span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
1367
+ </div>`;
1368
+ }
1369
+
1370
+ function firstUsefulLine(text) {
1371
+ return String(text || "")
1372
+ .split(/\n+/)
1373
+ .map((line) => line.trim())
1374
+ .filter(Boolean)[0] || "";
1375
+ }
1376
+
1377
+ function reviewWorkspace(route) {
1378
+ const task = (bundle.status?.tasks || []).find((item) => item.id === route.id);
1379
+ if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
1380
+ const walkthroughDoc = taskDocument(task, "__walkthrough__");
1381
+ const candidateDoc = taskDocument(task, "lesson_candidates.md");
1382
+ const reviewDoc = taskDocument(task, "review.md");
1383
+ const findingsDoc = taskDocument(task, "findings.md");
1384
+ return `<main class="review-workspace">
1385
+ <nav class="crumbs"><a href="#/review">${t("reviewQueue")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
1386
+ <section class="detail-hero review-hero">
1387
+ <div>
1388
+ <p class="eyebrow">${t("reviewWorkspace")}</p>
1389
+ <h2>${escapeHtml(task.title)}</h2>
1390
+ <p>${escapeHtml(task.path)}</p>
1391
+ </div>
1392
+ <div class="review-hero-tags">
1393
+ ${tag(task.lifecycleState || "unknown")}
1394
+ ${tag(task.reviewStatus || "missing")}
1395
+ ${tag(task.lessonCandidateStatus || "missing")}
1396
+ </div>
1397
+ </section>
1398
+ <section class="review-workspace-grid">
1399
+ <article class="review-workspace-main stack">
1400
+ ${reviewDocPanel("walkthrough", walkthroughDoc, task.walkthroughPath)}
1401
+ ${reviewDocPanel("lessonCandidates", candidateDoc, task.lessonCandidatePath)}
1402
+ ${reviewDocPanel("review", reviewDoc, task.reviewPath)}
1403
+ ${reviewDocPanel("findings", findingsDoc, task.findingsPath)}
1404
+ </article>
1405
+ <aside class="review-workspace-side stack">
1406
+ ${reviewActionPanel(task, { mode: "workspace" })}
1407
+ ${taskStateSummary(task)}
1408
+ ${openFindings(task)}
1409
+ ${evidenceList(task)}
1410
+ </aside>
1411
+ </section>
1412
+ </main>`;
1413
+ }
1414
+
1415
+ function reviewDocPanel(key, doc, fallbackPath = "") {
1416
+ return `<section class="doc-section review-doc-panel">
1417
+ <div class="section-head">
1418
+ <div>
1419
+ <p class="eyebrow">${escapeHtml(fallbackPath || "")}</p>
1420
+ <h2>${t(key)}</h2>
1421
+ </div>
1422
+ ${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
1423
+ </div>
1424
+ <div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
1425
+ </section>`;
1426
+ }
1427
+
1428
+ function migrationPanel() {
1429
+ const advice = warningQueue();
1430
+ const missingBriefs = advice.filter((warning) => warning.type === "missing-brief").length;
1431
+ if (advice.length === 0 && missingBriefs === 0) return "";
1432
+ const groups = groupBy(advice, (item) => item.category || "Advice");
1433
+ const categories = Object.entries(groups).slice(0, 6);
1434
+ return `<section class="migration-panel">
1435
+ <div class="section-head">
1436
+ <div>
1437
+ <p class="eyebrow">${t("migration")}</p>
1438
+ <h2>${t("migrationWorkbench")}</h2>
1439
+ </div>
1440
+ <span>${advice.length} ${t("advice")} · ${missingBriefs} ${t("briefMissing")}</span>
1441
+ </div>
1442
+ <div class="migration-grid">
1443
+ ${categories.map(([category, items]) => `<button data-warning-filter="${escapeAttr(category)}" class="${state.warningFilter === category ? "active" : ""}"><strong>${escapeHtml(category)}</strong><p>${items.length} ${t("items")}</p></button>`).join("")}
1444
+ ${missingBriefs > 0 ? `<div><strong>${t("visibilityLayer")}</strong><p>${missingBriefs} ${t("missingBriefs")}</p></div>` : ""}
1445
+ </div>
1446
+ ${migrationWarningWorkbench(advice)}
1447
+ </section>`;
1448
+ }
1449
+
1450
+ function migrationWarningWorkbench(advice) {
1451
+ const groups = groupBy(advice, (item) => item.category || "Advice");
1452
+ const filters = ["all", ...Object.keys(groups).sort(), ...new Set(advice.map((item) => item.type).filter(Boolean)), "active-task-contracts", "strict-cutover"];
1453
+ const filtered = state.warningFilter === "all" ? advice : advice.filter((item) => (item.category || "Advice") === state.warningFilter || item.phase === state.warningFilter || item.type === state.warningFilter);
1454
+ const pageCount = Math.max(1, Math.ceil(filtered.length / warningPageSize));
1455
+ const page = Math.min(Math.max(1, Number(state.warningPage) || 1), pageCount);
1456
+ const visible = filtered.slice((page - 1) * warningPageSize, page * warningPageSize);
1457
+ return `<div class="warning-workbench">
1458
+ <div class="warning-toolbar">
1459
+ <select data-warning-filter-select aria-label="${t("warningFilter")}">
1460
+ ${filters.map((filter) => `<option value="${escapeAttr(filter)}" ${state.warningFilter === filter ? "selected" : ""}>${filter === "all" ? t("allWarnings") : escapeHtml(filter)}</option>`).join("")}
1461
+ </select>
1462
+ <span>${t("showing")} ${visible.length ? (page - 1) * warningPageSize + 1 : 0}-${Math.min(page * warningPageSize, filtered.length)} / ${filtered.length}</span>
1463
+ ${pager("warning", page, pageCount)}
1464
+ </div>
1465
+ <div class="warning-list">
1466
+ ${visible.map(warningRow).join("") || emptyState(t("noWarnings"))}
1467
+ </div>
1468
+ </div>`;
1469
+ }
1470
+
1471
+ function migrationSummaryPanel() {
1472
+ const advice = warningQueue();
1473
+ const summary = bundle.status?.summary || {};
1474
+ if (advice.length === 0 && summary.fullCutoverEligible) {
1475
+ return `<section class="migration-panel">
1476
+ <div class="section-head">
1477
+ <div>
1478
+ <p class="eyebrow">${t("migration")}</p>
1479
+ <h2>${t("fullCutover")}</h2>
1480
+ </div>
1481
+ <span>${t("ready")}</span>
1482
+ </div>
1483
+ ${emptyState(t("noWarnings"))}
1484
+ </section>`;
308
1485
  }
309
- const docs = docsForSelection(selection);
310
- const active = docs.find((doc) => doc.tab === state.tab) || docs[0];
311
- return `<aside class="drawer">
312
- <div class="drawer-head"><h2>${escapeHtml(selection.label)}</h2><p class="muted">${escapeHtml(active?.path || "")}</p></div>
313
- <div class="drawer-tabs">${docs.map((doc) => `<button data-tab="${doc.tab}" class="${active?.tab === doc.tab ? "active" : ""}">${t(doc.tab)}</button>`).join("")}</div>
314
- <div class="drawer-mode"><button data-render-mode="rendered" class="${state.renderMode === "rendered" ? "active" : ""}">${t("rendered")}</button><button data-render-mode="source" class="${state.renderMode === "source" ? "active" : ""}">${t("source")}</button></div>
315
- <div class="drawer-body"><article class="markdown">${active ? window.HarnessMarkdown.render(active.content, state.renderMode) : t("noSelection")}</article></div>
316
- </aside>`;
317
- }
318
-
319
- function docsForSelection(selection) {
320
- if (selection.type !== "task") return [];
321
- const task = (bundle.status?.tasks || []).find((item) => item.id === selection.id);
322
- if (!task) return [];
323
- return taskDocTabs.map(([tab, suffix]) => {
324
- const doc = findDocument(`${task.path}/${suffix}`);
325
- return doc ? { tab, ...doc } : null;
326
- }).filter(Boolean);
1486
+ const cards = [
1487
+ [t("advice"), advice.length],
1488
+ [t("legacyVisualOnly"), summary.legacyVisualOnlyCount || 0],
1489
+ [t("weakBrief"), summary.weakBriefCount || 0],
1490
+ [t("blockers"), bundle.status?.checkState?.failures || 0],
1491
+ ];
1492
+ return `<section class="migration-panel">
1493
+ <div class="section-head">
1494
+ <div>
1495
+ <p class="eyebrow">${t("migration")}</p>
1496
+ <h2>${t("migrationSummary")}</h2>
1497
+ </div>
1498
+ <a href="#/tasks">${t("openTaskIndex")}</a>
1499
+ </div>
1500
+ <div class="migration-grid">
1501
+ ${cards.map(([title, count]) => `<a href="#/tasks"><strong>${escapeHtml(title)}</strong><p>${count} ${t("items")}</p></a>`).join("")}
1502
+ </div>
1503
+ ${migrationWarningWorkbench(advice)}
1504
+ </section>`;
1505
+ }
1506
+
1507
+ function warningRow(warning) {
1508
+ const affected = warning.affectedPaths?.length ? warning.affectedPaths.join(", ") : warning.affected;
1509
+ return `<article class="warning-row">
1510
+ <div>
1511
+ <strong>${escapeHtml(warning.id)} · ${escapeHtml(warning.title)}</strong>
1512
+ <p>${escapeHtml(affected || "project")}</p>
1513
+ </div>
1514
+ <span>${tag(warning.priority || warning.severity)}</span>
1515
+ <span>${escapeHtml(warning.status || "open")}</span>
1516
+ <span>${escapeHtml(warning.fixability || "manual")}</span>
1517
+ <span>${escapeHtml(warning.phase || "triage")}</span>
1518
+ <p>${escapeHtml(warning.requiredAction || warning.detail || "")} · ${t("confidence")}: ${escapeHtml(warning.confidence || "medium")}</p>
1519
+ </article>`;
1520
+ }
1521
+
1522
+ function warningQueue() {
1523
+ const adoptionWarnings = (bundle.adoption?.warnings || []).map((warning) => ({ ...warning }));
1524
+ const existingBriefPaths = new Set(adoptionWarnings.filter((warning) => warning.type === "missing-brief").map((warning) => warning.affected));
1525
+ const briefWarnings = (bundle.status?.tasks || [])
1526
+ .filter((task) => task.briefSource !== "standalone")
1527
+ .filter((task) => !existingBriefPaths.has(task.path))
1528
+ .map((task, index) => ({
1529
+ id: `VB-${String(index + 1).padStart(3, "0")}`,
1530
+ category: "Visibility Layer",
1531
+ type: "missing-brief",
1532
+ scope: "task",
1533
+ priority: (typeof isActiveTaskState === "function" && isActiveTaskState(task.state)) || ["planned", "not_started"].includes(task.state) ? "P2" : "P3",
1534
+ phase: "active-task-contracts",
1535
+ fixability: "guided",
1536
+ status: "open",
1537
+ confidence: task.state === "unknown" ? "medium" : "high",
1538
+ severity: "advice",
1539
+ title: t("visibilityBriefMissing"),
1540
+ affected: task.path,
1541
+ affectedPaths: [task.path],
1542
+ requiredAction: t("addVisibilityBrief"),
1543
+ detail: `${task.id} ${task.title}`,
1544
+ }));
1545
+ return [...adoptionWarnings, ...briefWarnings].sort(warningSort);
1546
+ }
1547
+
1548
+ function warningSort(left, right) {
1549
+ const priorityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
1550
+ const fixRank = { template: 0, guided: 1, "human-evidence": 2, decision: 3, manual: 4 };
1551
+ return (priorityRank[left.priority] ?? 9) - (priorityRank[right.priority] ?? 9)
1552
+ || (fixRank[left.fixability] ?? 9) - (fixRank[right.fixability] ?? 9)
1553
+ || String(left.phase || "").localeCompare(String(right.phase || ""))
1554
+ || String(left.id || "").localeCompare(String(right.id || ""));
1555
+ }
1556
+
1557
+ function pager(kind, page, pageCount, group = "") {
1558
+ if (pageCount <= 1) return `<span class="pager muted">${page}/${pageCount}</span>`;
1559
+ const groupAttr = group ? ` data-page-group="${escapeAttr(group)}"` : "";
1560
+ return `<div class="pager">
1561
+ <button data-page-kind="${kind}" data-page="${page - 1}"${groupAttr} ${page <= 1 ? "disabled" : ""}>${t("prevPage")}</button>
1562
+ <span>${page}/${pageCount}</span>
1563
+ <button data-page-kind="${kind}" data-page="${page + 1}"${groupAttr} ${page >= pageCount ? "disabled" : ""}>${t("nextPage")}</button>
1564
+ </div>`;
1565
+ }
1566
+
1567
+ function lessonPanel() {
1568
+ const lessons = lessonDocuments();
1569
+ return `<section class="lesson-panel">
1570
+ <div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
1571
+ <div class="lesson-list" style="padding-top: 10px;">
1572
+ ${lessons.map((lesson) => {
1573
+ return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
1574
+ <strong>${escapeHtml(lesson.id)}</strong>
1575
+ <p>${escapeHtml(lesson.title || lesson.path)}</p>
1576
+ </div>`;
1577
+ }).join("") || emptyState(t("noLessons"))}
1578
+ </div>
1579
+ </section>`;
1580
+ }
1581
+
1582
+ function lessonDocuments() {
1583
+ return (bundle.documents?.documents || [])
1584
+ .filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
1585
+ .map((doc) => {
1586
+ const id = lessonIdFromDocument(doc);
1587
+ return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
1588
+ })
1589
+ .filter((lesson) => lesson.id)
1590
+ .sort((left, right) => String(right.id).localeCompare(String(left.id)));
1591
+ }
1592
+
1593
+ function lessonIdFromDocument(doc) {
1594
+ const content = doc?.content || "";
1595
+ const path = doc?.path || "";
1596
+ return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
1597
+ || path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
1598
+ || "";
1599
+ }
1600
+
1601
+ function healthPanel() {
1602
+ const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
1603
+ return `<section class="health-panel">
1604
+ <div><h2>${t("releaseHealth")}</h2><p>${escapeHtml(bundle.status?.mode || "unknown")} · schema ${escapeHtml(bundle.status?.schemaVersion || "n/a")}</p></div>
1605
+ <div class="health-lists">
1606
+ <details ${details.failures?.length ? "open" : ""}><summary>${t("failures")} (${details.failures?.length || 0})</summary>${list(details.failures)}</details>
1607
+ <details><summary>${t("warnings")} (${details.warnings?.length || 0})</summary>${list(details.warnings?.slice(0, 40))}</details>
1608
+ </div>
1609
+ </section>`;
1610
+ }
1611
+
1612
+ function taskDocument(task, fileName) {
1613
+ if (fileName === "__walkthrough__" && task.walkthroughPath) return findDocument(task.walkthroughPath);
1614
+ return findDocument(`${task.path}/${fileName}`);
327
1615
  }
328
1616
 
329
1617
  function findDocument(pathSuffix) {
330
- return (bundle.documents?.documents || []).find((doc) => doc.path.endsWith(pathSuffix));
1618
+ return (bundle.documents?.documents || []).find((doc) => doc.path.endsWith(pathSuffix) || doc.path === pathSuffix);
331
1619
  }
332
1620
 
333
- function clickable(label, type, id) {
334
- return `<button class="linklike" data-select-type="${type}" data-select-id="${id}" data-select-label="${escapeAttr(label)}">${escapeHtml(label)}</button>`;
1621
+ function mermaidLabel(id) {
1622
+ const node = (bundle.graph?.nodes || []).find((item) => item.id === id);
1623
+ return String(node?.label || id).replaceAll('"', "'").slice(0, 48);
335
1624
  }
336
1625
 
337
- function tag(value, text = value) {
1626
+ function mermaidId(value) {
1627
+ return `N_${String(value).replace(/[^A-Za-z0-9_]/g, "_")}`;
1628
+ }
1629
+
1630
+ function progressBar(value) {
1631
+ const score = Math.max(0, Math.min(100, Number(value) || 0));
1632
+ return `<div class="progress" aria-label="${score}%"><i style="width:${score}%"></i></div>`;
1633
+ }
1634
+
1635
+ function tag(value) {
338
1636
  const raw = String(value || "unknown");
339
- const klass = /fail|blocked|open/i.test(raw) ? "fail" : /warn|advice|planned|missing/i.test(raw) ? "warn" : /pass|done|present|verified/i.test(raw) ? "pass" : "";
340
- return `<span class="tag ${klass}">${escapeHtml(text)}</span>`;
1637
+ const klass = /fail|blocked|open/i.test(raw) ? "fail" : /warn|advice|planned|missing|unknown/i.test(raw) ? "warn" : /pass|done|present|verified|review|in_progress/i.test(raw) ? "pass" : "";
1638
+ return `<span class="tag ${klass}">${escapeHtml(label(raw))}</span>`;
341
1639
  }
342
1640
 
343
1641
  function label(value) {
344
- const labels = {
345
- pass: "pass",
346
- warn: "warn",
347
- fail: "fail",
348
- in_progress: "in progress",
349
- planned: "planned",
350
- done: "done",
351
- blocked: "blocked",
352
- missing: "missing",
353
- present: "present",
354
- closed: "closed",
355
- advice: "advice",
356
- standalone: "standalone",
357
- legacy: "legacy",
358
- "task-review": "review",
359
- "task-progress": "progress",
360
- "regression-ssot": "regression",
361
- "cadence-ledger": "cadence",
362
- unknown: "unknown",
363
- };
364
- return labels[value] || value;
1642
+ return t(`state_${value}`) || String(value || "unknown").replaceAll("_", " ");
365
1643
  }
366
1644
 
367
- function progress(value) {
368
- const score = Math.max(0, Math.min(100, Number(value) || 0));
369
- return `<span>${score}%</span><div class="bar"><i style="width:${score}%"></i></div>`;
1645
+ function list(items = []) {
1646
+ return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("") || `<li>${t("none")}</li>`}</ul>`;
370
1647
  }
371
1648
 
372
- function evidenceHealth(tasks) {
373
- const phases = tasks.flatMap((task) => task.phases || []).filter((phase) => phase.state !== "skipped");
374
- if (phases.length === 0) return 0;
375
- const score = phases.reduce((sum, phase) => {
376
- if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
377
- if (phase.evidenceStatus === "partial") return sum + 50;
378
- return sum;
379
- }, 0);
380
- return Math.round(score / phases.length);
1649
+ function emptyState(text) {
1650
+ return `<div class="empty">${escapeHtml(text)}</div>`;
1651
+ }
1652
+
1653
+ function projectName() {
1654
+ return bundle.status?.project?.name || "Harness";
1655
+ }
1656
+
1657
+ function themeLabel() {
1658
+ return state.theme === "dark" ? t("light") : state.theme === "light" ? t("system") : t("dark");
381
1659
  }
382
1660
 
383
1661
  function groupBy(items, fn) {
@@ -389,35 +1667,517 @@ function groupBy(items, fn) {
389
1667
  }, {});
390
1668
  }
391
1669
 
1670
+ function canUseWorkbenchAction(action) {
1671
+ return state.runtime?.mode === "workbench" && (state.runtime?.writableActions || []).includes(action);
1672
+ }
1673
+
1674
+ window.setModulePage = function(moduleKey, page) {
1675
+ state.modulePages = state.modulePages || {};
1676
+ state.modulePages[moduleKey] = page;
1677
+ app();
1678
+ };
1679
+
392
1680
  function bind() {
393
- document.querySelectorAll("[data-page]").forEach((button) => button.addEventListener("click", () => {
394
- state.page = button.dataset.page;
395
- state.selected = null;
1681
+ document.querySelectorAll("[data-search]").forEach((input) => input.addEventListener("input", () => {
1682
+ state.query = input.value;
1683
+ state.taskPageByGroup = {};
1684
+ state.taskGroupPage = 1;
396
1685
  app();
397
1686
  }));
398
- document.querySelectorAll("[data-theme]").forEach((button) => button.addEventListener("click", () => {
399
- state.theme = button.dataset.theme;
400
- localStorage.setItem("harness.theme", state.theme);
1687
+ document.querySelectorAll("[data-state-filter]").forEach((select) => select.addEventListener("change", () => {
1688
+ state.taskState = select.value;
1689
+ state.taskPageByGroup = {};
1690
+ state.taskGroupPage = 1;
1691
+ app();
1692
+ }));
1693
+ document.querySelectorAll("[data-group-mode]").forEach((select) => select.addEventListener("change", () => {
1694
+ state.taskGroupMode = select.value;
1695
+ state.taskPageByGroup = {};
1696
+ state.taskGroupPage = 1;
1697
+ app();
1698
+ }));
1699
+ document.querySelectorAll("[data-layout]").forEach((btn) => btn.addEventListener("click", () => {
1700
+ state.taskLayout = btn.dataset.layout;
1701
+ localStorage.setItem("harness.taskLayout", state.taskLayout);
1702
+ app();
1703
+ }));
1704
+ document.querySelectorAll("[data-task-sort-order]").forEach((btn) => btn.addEventListener("click", () => {
1705
+ state.taskSortOrder = btn.dataset.taskSortOrder === "asc" ? "asc" : "desc";
1706
+ localStorage.setItem("harness.taskSortOrder", state.taskSortOrder);
1707
+ state.taskPageByGroup = {};
1708
+ state.taskGroupPage = 1;
401
1709
  app();
402
1710
  }));
403
- document.querySelectorAll("[data-density]").forEach((button) => button.addEventListener("click", () => {
404
- state.density = button.dataset.density;
405
- localStorage.setItem("harness.density", state.density);
1711
+ document.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
1712
+ state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
406
1713
  app();
407
1714
  }));
408
- document.querySelectorAll("[data-select-type]").forEach((button) => button.addEventListener("click", () => {
409
- state.selected = { type: button.dataset.selectType, id: button.dataset.selectId, label: button.dataset.selectLabel };
410
- state.tab = "plan";
1715
+ document.querySelectorAll("[data-warning-filter]").forEach((button) => button.addEventListener("click", () => {
1716
+ state.warningFilter = button.dataset.warningFilter || "all";
1717
+ state.warningPage = 1;
411
1718
  app();
412
1719
  }));
413
- document.querySelectorAll("[data-tab]").forEach((button) => button.addEventListener("click", () => {
414
- state.tab = button.dataset.tab;
1720
+ document.querySelectorAll("[data-warning-filter-select]").forEach((select) => select.addEventListener("change", () => {
1721
+ state.warningFilter = select.value;
1722
+ state.warningPage = 1;
415
1723
  app();
416
1724
  }));
417
- document.querySelectorAll("[data-render-mode]").forEach((button) => button.addEventListener("click", () => {
418
- state.renderMode = button.dataset.renderMode;
1725
+ document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
1726
+ state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
1727
+ state.reviewQueuePage = 1;
419
1728
  app();
420
1729
  }));
1730
+ document.querySelectorAll("[data-review-reason-filter]").forEach((select) => select.addEventListener("change", () => {
1731
+ state.reviewReasonFilter = select.value || "all";
1732
+ state.reviewQueuePage = 1;
1733
+ app();
1734
+ }));
1735
+ document.querySelectorAll("[data-review-sort]").forEach((select) => select.addEventListener("change", () => {
1736
+ state.reviewSort = select.value || "queue";
1737
+ state.reviewQueuePage = 1;
1738
+ app();
1739
+ }));
1740
+ document.querySelectorAll("[data-page-kind]").forEach((button) => button.addEventListener("click", () => {
1741
+ const page = Math.max(1, Number(button.dataset.page) || 1);
1742
+ if (button.dataset.pageKind === "warning") state.warningPage = page;
1743
+ if (button.dataset.pageKind === "task-groups") state.taskGroupPage = page;
1744
+ if (button.dataset.pageKind === "task") state.taskPageByGroup[button.dataset.pageGroup || ""] = page;
1745
+ if (button.dataset.pageKind === "review") state.reviewQueuePage = page;
1746
+ app();
1747
+ }));
1748
+ document.querySelectorAll("[data-runway-phase]").forEach((link) => link.addEventListener("click", () => {
1749
+ const phase = link.dataset.runwayPhase || "all";
1750
+ if (phase === "module-classification") state.taskGroupMode = "module";
1751
+ if (["triage", "active-task-contracts", "strict-cutover"].includes(phase)) state.warningFilter = phase === "triage" ? "all" : phase;
1752
+ state.warningPage = 1;
1753
+ state.taskGroupPage = 1;
1754
+ if (link.getAttribute("href") === "#/") app();
1755
+ }));
1756
+ document.querySelectorAll("[data-theme-toggle]").forEach((button) => button.addEventListener("click", () => {
1757
+ state.theme = state.theme === "dark" ? "light" : state.theme === "light" ? "system" : "dark";
1758
+ localStorage.setItem("harness.theme", state.theme);
1759
+ app();
1760
+ }));
1761
+ document.querySelectorAll("[data-language-toggle]").forEach((button) => button.addEventListener("click", () => {
1762
+ setLocale(locale === "zh" ? "en" : "zh");
1763
+ app();
1764
+ }));
1765
+ document.querySelectorAll("[data-open-drawer]").forEach((el) => el.addEventListener("click", (e) => {
1766
+ e.preventDefault();
1767
+ const taskId = el.dataset.openDrawer;
1768
+ openDrawer(taskId);
1769
+ }));
1770
+ bindCopyTaskNameButtons(document);
1771
+ bindRepairPromptButtons(document);
1772
+ bindLessonSedimentationButtons(document);
1773
+ document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
1774
+ e.preventDefault();
1775
+ const lessonId = el.dataset.openLessonDrawer;
1776
+ openLessonDrawer(lessonId);
1777
+ }));
1778
+ document.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
1779
+ const overlay = document.getElementById("drawer-overlay");
1780
+ if (overlay) overlay.addEventListener("click", closeDrawer);
1781
+ }
1782
+
1783
+ async function loadRuntime() {
1784
+ if (state.runtimeLoaded || window.__HARNESS_WORKBENCH__ !== true || !/^https?:$/.test(window.location.protocol)) return;
1785
+ state.runtimeLoaded = true;
1786
+ try {
1787
+ const response = await fetch("/api/runtime", { cache: "no-store" });
1788
+ if (!response.ok) return;
1789
+ state.runtime = await response.json();
1790
+ startRuntimePolling();
1791
+ app();
1792
+ } catch {
1793
+ state.runtime = { mode: "static", csrfToken: "", writableActions: [] };
1794
+ }
1795
+ }
1796
+
1797
+ function startRuntimePolling() {
1798
+ if (!state.runtime?.autoRefresh || state.runtimePoller) return;
1799
+ state.runtimePoller = setInterval(async () => {
1800
+ try {
1801
+ const response = await fetch("/api/runtime", { cache: "no-store" });
1802
+ if (!response.ok) return;
1803
+ const nextRuntime = await response.json();
1804
+ if (state.runtime?.snapshotVersion && nextRuntime.snapshotVersion !== state.runtime.snapshotVersion) {
1805
+ window.location.reload();
1806
+ return;
1807
+ }
1808
+ state.runtime = nextRuntime;
1809
+ } catch {
1810
+ clearInterval(state.runtimePoller);
1811
+ state.runtimePoller = null;
1812
+ }
1813
+ }, 1500);
1814
+ }
1815
+
1816
+ async function completeReviewFromDashboard(taskId) {
1817
+ const result = document.querySelector(`[data-review-result="${CSS.escape(taskId)}"]`);
1818
+ const checkbox = document.querySelector(`[data-review-confirm-check="${CSS.escape(taskId)}"]`);
1819
+ const confirmInput = document.querySelector(`[data-review-confirm-text="${CSS.escape(taskId)}"]`);
1820
+ const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
1821
+ if (!checkbox?.checked) {
1822
+ if (result) result.textContent = t("reviewChecklistRequired");
1823
+ return;
1824
+ }
1825
+ if (!confirmInput?.value || ![task?.shortId, task?.id].includes(confirmInput.value.trim())) {
1826
+ if (result) result.textContent = t("reviewConfirmTextMismatch");
1827
+ return;
1828
+ }
1829
+ if (result) result.textContent = t("reviewSubmitting");
1830
+ try {
1831
+ const response = await fetch("/api/tasks/review-complete", {
1832
+ method: "POST",
1833
+ headers: {
1834
+ "content-type": "application/json",
1835
+ "x-harness-csrf": state.runtime?.csrfToken || "",
1836
+ },
1837
+ body: JSON.stringify({
1838
+ taskId,
1839
+ confirmText: confirmInput.value.trim(),
1840
+ reviewer: "Human Reviewer",
1841
+ message: "confirmed from dashboard workbench",
1842
+ }),
1843
+ });
1844
+ const payload = await response.json();
1845
+ if (!response.ok) throw new Error(payload.error || t("reviewCompleteFailed"));
1846
+ if (result) result.textContent = t("reviewCompleteSuccess");
1847
+ setTimeout(() => window.location.reload(), 500);
1848
+ } catch (error) {
1849
+ if (result) result.textContent = `${t("reviewCompleteFailed")}: ${error.message}`;
1850
+ }
1851
+ }
1852
+
1853
+ function renderDrawerContent(taskId) {
1854
+ const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
1855
+ if (!task) return `<div class="empty">${t("taskNotFound")}</div>`;
1856
+
1857
+ const header = `
1858
+ <div class="task-drawer-header">
1859
+ <div>
1860
+ <h2>${escapeHtml(task.title)}</h2>
1861
+ <p style="font-family: var(--font-mono); font-size: 11px; margin: 4px 0 0; color: var(--muted);">${escapeHtml(task.id)}</p>
1862
+ ${taskCopyButton(task, "detail-copy")}
1863
+ </div>
1864
+ <button class="btn-close" data-close-drawer>×</button>
1865
+ </div>
1866
+ `;
1867
+
1868
+ const timeline = phaseTimeline(task);
1869
+ const documents = taskDocumentLibrary(task, "");
1870
+ const findings = openFindings(task);
1871
+ const evidence = evidenceList(task);
1872
+
1873
+ const body = `
1874
+ <div class="task-drawer-body stack">
1875
+ <div class="drawer-task-summary">
1876
+ <div>
1877
+ <span>${t("statOverall")}</span>
1878
+ <strong>${task.completion}%</strong>
1879
+ </div>
1880
+ <a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger">${t("fullView")}</a>
1881
+ </div>
1882
+ ${taskStateSummary(task)}
1883
+ ${reviewActionPanel(task, { mode: "summary" })}
1884
+ ${lessonCandidatePanel(task, { context: "drawer" })}
1885
+ ${timeline}
1886
+ ${documents}
1887
+ ${findings}
1888
+ ${evidence}
1889
+ </div>
1890
+ `;
1891
+
1892
+ return header + body;
1893
+ }
1894
+
1895
+ function openDrawer(taskId) {
1896
+ const drawer = document.getElementById("task-drawer");
1897
+ const overlay = document.getElementById("drawer-overlay");
1898
+ if (!drawer || !overlay) return;
1899
+ drawer.innerHTML = renderDrawerContent(taskId);
1900
+ drawer.classList.add("active");
1901
+ overlay.classList.add("active");
1902
+
1903
+ drawer.querySelector("[data-close-drawer]").addEventListener("click", closeDrawer);
1904
+ drawer.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
1905
+ state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
1906
+ openDrawer(taskId);
1907
+ }));
1908
+ bindCopyTaskNameButtons(drawer);
1909
+ bindRepairPromptButtons(drawer);
1910
+ bindLessonSedimentationButtons(drawer);
1911
+ drawer.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
1912
+ }
1913
+
1914
+ function bindCopyTaskNameButtons(root) {
1915
+ root.querySelectorAll("[data-copy-task-name]").forEach((button) => button.addEventListener("click", async (event) => {
1916
+ event.preventDefault();
1917
+ event.stopPropagation();
1918
+ const taskName = button.dataset.copyTaskName || "";
1919
+ const defaultText = t("copyTaskNameShort");
1920
+ try {
1921
+ await copyText(taskName);
1922
+ button.textContent = t("copyTaskNameSuccess");
1923
+ } catch {
1924
+ button.textContent = t("copyTaskNameFailed");
1925
+ }
1926
+ window.setTimeout(() => {
1927
+ button.textContent = defaultText;
1928
+ }, 1400);
1929
+ }));
1930
+ }
1931
+
1932
+ function bindRepairPromptButtons(root) {
1933
+ root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
1934
+ event.preventDefault();
1935
+ event.stopPropagation();
1936
+ const prompt = button.dataset.repairPrompt || "";
1937
+ const defaultText = t("copyRepairPrompt");
1938
+ try {
1939
+ await copyText(prompt);
1940
+ button.textContent = t("copyRepairPromptSuccess");
1941
+ } catch {
1942
+ button.textContent = t("copyTaskNameFailed");
1943
+ }
1944
+ window.setTimeout(() => {
1945
+ button.textContent = defaultText;
1946
+ }, 1400);
1947
+ }));
1948
+ }
1949
+
1950
+ function bindLessonSedimentationButtons(root) {
1951
+ root.querySelectorAll("[data-copy-lesson-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
1952
+ event.preventDefault();
1953
+ event.stopPropagation();
1954
+ const prompt = button.dataset.lessonPrompt || "";
1955
+ const defaultText = t("copyLessonPrompt");
1956
+ try {
1957
+ await copyText(prompt);
1958
+ button.textContent = t("copyRepairPromptSuccess");
1959
+ } catch {
1960
+ button.textContent = t("copyTaskNameFailed");
1961
+ }
1962
+ window.setTimeout(() => {
1963
+ button.textContent = defaultText;
1964
+ }, 1400);
1965
+ }));
1966
+ root.querySelectorAll("[data-create-lesson-sedimentation]").forEach((button) => button.addEventListener("click", async (event) => {
1967
+ event.preventDefault();
1968
+ event.stopPropagation();
1969
+ await createLessonSedimentationFromDashboard(button);
1970
+ }));
1971
+ }
1972
+
1973
+ async function createLessonSedimentationFromDashboard(button) {
1974
+ const taskId = button.dataset.createLessonSedimentation || "";
1975
+ const candidateId = button.dataset.candidateId || "";
1976
+ const result = document.querySelector(`[data-lesson-result="${CSS.escape(`${taskId}:${candidateId}`)}"]`);
1977
+ if (result) result.textContent = t("lessonTaskCreating");
1978
+ button.disabled = true;
1979
+ try {
1980
+ const response = await fetch("/api/tasks/lesson-sedimentation", {
1981
+ method: "POST",
1982
+ headers: {
1983
+ "content-type": "application/json",
1984
+ "x-harness-csrf": state.runtime?.csrfToken || "",
1985
+ },
1986
+ body: JSON.stringify({ taskId, candidateId }),
1987
+ });
1988
+ const payload = await response.json();
1989
+ if (!response.ok) throw payload;
1990
+ if (result) {
1991
+ result.innerHTML = lessonSedimentationSuccess(payload);
1992
+ bindLessonSedimentationButtons(result);
1993
+ result.scrollIntoView({ block: "center", inline: "nearest" });
1994
+ }
1995
+ } catch (error) {
1996
+ button.disabled = false;
1997
+ if (result) result.innerHTML = lessonSedimentationFailure(error);
1998
+ }
1999
+ }
2000
+
2001
+ function lessonSedimentationSuccess(payload) {
2002
+ const followUp = payload?.followUpTask || {};
2003
+ const prompt = payload?.prompt || "";
2004
+ const taskId = followUp.id || "";
2005
+ const openHref = taskId ? `#/tasks/${encodeURIComponent(taskId)}` : "#/review";
2006
+ return `<div class="workbench-action-result success">
2007
+ <strong>${escapeHtml(t("lessonTaskCreated"))}</strong>
2008
+ ${taskId ? `<a href="${openHref}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
2009
+ ${prompt ? `<button data-copy-lesson-prompt="${escapeAttr(taskId || "follow-up")}" data-lesson-prompt="${escapeAttr(prompt)}">${escapeHtml(t("copyLessonPrompt"))}</button>` : ""}
2010
+ </div>`;
2011
+ }
2012
+
2013
+ function lessonSedimentationFailure(error) {
2014
+ const message = error?.error || error?.message || t("lessonTaskCreateFailed");
2015
+ const recovery = Array.isArray(error?.recovery) ? error.recovery : [];
2016
+ const details = error?.details || {};
2017
+ const existingTask = details.followUpTask || details.existingTask || "";
2018
+ return `<div class="workbench-action-result failed">
2019
+ <strong>${escapeHtml(t("lessonTaskCreateFailed"))}</strong>
2020
+ <span>${escapeHtml(message)}</span>
2021
+ ${existingTask ? `<a href="#/tasks/${encodeURIComponent(existingTask)}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
2022
+ ${recovery.length ? `<ul>${recovery.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : ""}
2023
+ </div>`;
2024
+ }
2025
+
2026
+ async function copyText(text) {
2027
+ if (navigator.clipboard?.writeText) {
2028
+ await navigator.clipboard.writeText(text);
2029
+ return;
2030
+ }
2031
+ const textarea = document.createElement("textarea");
2032
+ textarea.value = text;
2033
+ textarea.setAttribute("readonly", "");
2034
+ textarea.style.position = "fixed";
2035
+ textarea.style.left = "-9999px";
2036
+ document.body.appendChild(textarea);
2037
+ textarea.select();
2038
+ const copied = document.execCommand("copy");
2039
+ textarea.remove();
2040
+ if (!copied) throw new Error("copy failed");
2041
+ }
2042
+
2043
+ function renderLessonDrawerContent(lessonId) {
2044
+ const lesson = lessonDocuments().find((item) => item.id === lessonId);
2045
+
2046
+ if (!lesson) {
2047
+ return `<div class="task-drawer-header">
2048
+ <h2>${escapeHtml(lessonId)}</h2>
2049
+ <button class="btn-close" data-close-drawer>×</button>
2050
+ </div>
2051
+ <div class="task-drawer-body">
2052
+ <div class="empty">${t("lessonNotFound")}</div>
2053
+ </div>`;
2054
+ }
2055
+
2056
+ const doc = lesson.doc || findDocument(lesson.path);
2057
+
2058
+ const header = `
2059
+ <div class="task-drawer-header">
2060
+ <div>
2061
+ <h2>${escapeHtml(lessonId)}</h2>
2062
+ <p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(lesson.title || lesson.path)}</p>
2063
+ </div>
2064
+ <button class="btn-close" data-close-drawer>×</button>
2065
+ </div>
2066
+ `;
2067
+
2068
+ let markdownBody = "";
2069
+ if (doc && doc.content) {
2070
+ markdownBody = `<div class="markdown">${window.HarnessMarkdown.render(doc.content, "rendered")}</div>`;
2071
+ } else {
2072
+ markdownBody = `
2073
+ <div style="margin-bottom: 20px; background: var(--paper-2); padding: 16px; border-radius: 8px; border: 1px dashed var(--line);">
2074
+ <p style="margin: 0; font-size: 13px; color: var(--muted);">${t("lessonDocMissing")}</p>
2075
+ </div>
2076
+ `;
2077
+ }
2078
+
2079
+ const body = `
2080
+ <div class="task-drawer-body stack">
2081
+ ${markdownBody}
2082
+ </div>
2083
+ `;
2084
+
2085
+ return header + body;
2086
+ }
2087
+
2088
+ function openLessonDrawer(lessonId) {
2089
+ const drawer = document.getElementById("task-drawer");
2090
+ const overlay = document.getElementById("drawer-overlay");
2091
+ if (!drawer || !overlay) return;
2092
+ drawer.innerHTML = renderLessonDrawerContent(lessonId);
2093
+ drawer.classList.add("active");
2094
+ overlay.classList.add("active");
2095
+
2096
+ drawer.querySelector("[data-close-drawer]").addEventListener("click", closeDrawer);
2097
+ }
2098
+
2099
+ function closeDrawer() {
2100
+ const drawer = document.getElementById("task-drawer");
2101
+ const overlay = document.getElementById("drawer-overlay");
2102
+ if (drawer) drawer.classList.remove("active");
2103
+ if (overlay) overlay.classList.remove("active");
2104
+ }
2105
+
2106
+ function ledgerPanel() {
2107
+ const ledgerTable = (bundle.tables?.tables || []).find((table) => table.kind === "harness-ledger");
2108
+ const rows = ledgerTable?.rows || [];
2109
+
2110
+ let closedCount = 0;
2111
+ let openCount = 0;
2112
+ let blockedCount = 0;
2113
+
2114
+ let lessonsReviewed = 0;
2115
+ let lessonsTotal = 0;
2116
+
2117
+ let evidenceAudited = 0;
2118
+ let evidenceTotal = 0;
2119
+
2120
+ for (const row of rows) {
2121
+ const cells = row.cells || {};
2122
+ const status = String(cells.Status || cells["\u72b6\u6001"] || "").toLowerCase();
2123
+ if (status.includes("close") || status.includes("done") || status.includes("\u7ed3") || status.includes("\u5b8c")) {
2124
+ closedCount++;
2125
+ } else if (status.includes("block") || status.includes("\u963b")) {
2126
+ blockedCount++;
2127
+ } else {
2128
+ openCount++;
2129
+ }
2130
+
2131
+ const lesson = String(cells.Lessons || cells["\u7ecf\u9a8c"] || cells["\u7ecf\u9a8c\u5ba1\u67e5"] || cells["Lesson"] || "");
2132
+ if (lesson) {
2133
+ lessonsTotal++;
2134
+ if (lesson.toLowerCase().includes("pass") || lesson.includes("\u901a\u8fc7") || lesson.includes("\u5c31\u7eea") || lesson.toLowerCase().includes("checked") || lesson.toLowerCase().includes("done")) {
2135
+ lessonsReviewed++;
2136
+ }
2137
+ }
2138
+
2139
+ const evidence = String(cells.Evidence || cells["\u8bc1\u636e"] || cells["\u9a8c\u8bc1\u8bc1\u636e"] || cells["Evidence Checked"] || "");
2140
+ if (evidence) {
2141
+ evidenceTotal++;
2142
+ if (evidence.toLowerCase().includes("pass") || evidence.includes("\u901a\u8fc7") || evidence.toLowerCase().includes("present") || evidence.toLowerCase().includes("verified") || evidence.toLowerCase().includes("done")) {
2143
+ evidenceAudited++;
2144
+ }
2145
+ }
2146
+ }
2147
+
2148
+ const total = closedCount + openCount + blockedCount || 1;
2149
+ const closedPct = Math.round((closedCount / total) * 100);
2150
+ const openPct = Math.round((openCount / total) * 100);
2151
+ const blockedPct = total - closedPct - openPct;
2152
+
2153
+ const lessonsPct = lessonsTotal ? Math.round((lessonsReviewed / lessonsTotal) * 100) : 0;
2154
+ const evidencePct = evidenceTotal ? Math.round((evidenceAudited / evidenceTotal) * 100) : 0;
2155
+
2156
+ if (rows.length === 0) return "";
2157
+
2158
+ return `<section class="ledger-panel">
2159
+ <h2>${t("ssotLedger")}</h2>
2160
+ <div class="ledger-split-bar" title="${t("tagClosed")}: ${closedCount}, ${t("tagOpen")}: ${openCount}, ${t("tagBlocked")}: ${blockedCount}">
2161
+ <div class="ledger-split-segment closed" style="width: ${closedPct}%"></div>
2162
+ <div class="ledger-split-segment open" style="width: ${openPct}%"></div>
2163
+ <div class="ledger-split-segment blocked" style="width: ${Math.max(0, blockedPct)}%"></div>
2164
+ </div>
2165
+ <div class="ledger-split-legend">
2166
+ <span class="ledger-split-legend-item"><i class="ledger-split-legend-dot closed"></i>${t("tagClosed")} (${closedCount})</span>
2167
+ <span class="ledger-split-legend-item"><i class="ledger-split-legend-dot open"></i>${t("tagOpen")} (${openCount})</span>
2168
+ <span class="ledger-split-legend-item"><i class="ledger-split-legend-dot blocked"></i>${t("tagBlocked")} (${blockedCount})</span>
2169
+ </div>
2170
+ <div class="ledger-gauge-row">
2171
+ <div class="ledger-gauge-card">
2172
+ <span>${t("lessonsCheckRate")}</span>
2173
+ <strong>${lessonsPct}%</strong>
2174
+ </div>
2175
+ <div class="ledger-gauge-card">
2176
+ <span>${t("evidenceAuditRate")}</span>
2177
+ <strong>${evidencePct}%</strong>
2178
+ </div>
2179
+ </div>
2180
+ </section>`;
421
2181
  }
422
2182
 
423
2183
  function escapeHtml(value) {
@@ -432,4 +2192,6 @@ function escapeAttr(value) {
432
2192
  return escapeHtml(value).replaceAll("'", "&#39;");
433
2193
  }
434
2194
 
2195
+ window.addEventListener("hashchange", app);
435
2196
  app();
2197
+ loadRuntime();