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
@@ -0,0 +1,246 @@
1
+ function taskDetail(route) {
2
+ const taskId = route.id;
3
+ const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
4
+ if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
5
+ return `<main class="task-detail">
6
+ <nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
7
+ <section class="detail-hero">
8
+ <div>
9
+ <p class="eyebrow">${t("taskVisibility")}</p>
10
+ <h2>${escapeHtml(task.title)}</h2>
11
+ <p>${escapeHtml(task.path)}</p>
12
+ ${taskCopyButton(task, "detail-copy")}
13
+ </div>
14
+ <div class="detail-score">${task.completion}%</div>
15
+ </section>
16
+ ${taskStateSummary(task)}
17
+ ${phaseTimeline(task)}
18
+ <section class="detail-grid">
19
+ <article class="detail-main">
20
+ ${taskDocumentLibrary(task, route.doc)}
21
+ </article>
22
+ <aside class="detail-side">
23
+ ${reviewActionPanel(task, { mode: "summary" })}
24
+ ${lessonCandidatePanel(task, { context: "detail" })}
25
+ ${openFindings(task)}
26
+ ${evidenceList(task)}
27
+ ${documentTabs(task)}
28
+ </aside>
29
+ </section>
30
+ </main>`;
31
+ }
32
+
33
+ function taskStateSummary(task) {
34
+ return `<section class="task-state-summary">
35
+ <div>
36
+ <span>${t("legacyState")}</span>
37
+ ${tag(task.state)}
38
+ </div>
39
+ <div>
40
+ <span>${t("lifecycleState")}</span>
41
+ ${tag(task.lifecycleState || "unknown")}
42
+ </div>
43
+ <div>
44
+ <span>${t("reviewStatus")}</span>
45
+ ${tag(task.reviewStatus || "missing")}
46
+ </div>
47
+ <div>
48
+ <span>${t("sedimentationStatus")}</span>
49
+ ${tag(task.lessonCandidateStatus || "missing")}
50
+ </div>
51
+ <div>
52
+ <span>${t("closeoutStatus")}</span>
53
+ ${tag(task.closeoutStatus || "missing")}
54
+ </div>
55
+ <div>
56
+ <span>${t("lifecycleQueues")}</span>
57
+ ${(task.taskQueues || []).map(tag).join("") || tag("active")}
58
+ </div>
59
+ ${taskQueueReasonSummary(task)}
60
+ </section>`;
61
+ }
62
+
63
+ function taskQueueReasonSummary(task) {
64
+ const reasons = task.queueReasons || [];
65
+ if (!reasons.length) return "";
66
+ return `<div class="task-queue-reasons">
67
+ <span>${t("queueReasons")}</span>
68
+ <div class="review-reasons">
69
+ ${reasons.slice(0, 5).map(reviewReason).join("")}
70
+ </div>
71
+ </div>`;
72
+ }
73
+
74
+ function phaseTimeline(task) {
75
+ return `<section class="phase-timeline">
76
+ <h2>${t("phaseTimeline")}</h2>
77
+ ${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
78
+ <strong>${escapeHtml(phase.id)}</strong>
79
+ <span>${phase.completion}%</span>
80
+ <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
81
+ ${progressBar(phase.completion)}
82
+ </div>`).join("") || emptyState(t("noPhaseData"))}
83
+ </section>`;
84
+ }
85
+
86
+ function taskDocSection(task, fileName, title, required) {
87
+ const doc = taskDocument(task, fileName);
88
+ if (!doc && !required) return "";
89
+ return `<section class="doc-section">
90
+ <div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
91
+ <div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
92
+ </section>`;
93
+ }
94
+
95
+ function taskDocumentLibrary(task, selectedTab) {
96
+ const docs = orderedTaskDocuments(task);
97
+ if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
98
+ const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
99
+ return `<section class="doc-library">
100
+ <div class="section-head">
101
+ <div>
102
+ <p class="eyebrow">${t("taskDocuments")}</p>
103
+ <h2>${escapeHtml(t("sourceDocuments"))}</h2>
104
+ </div>
105
+ <button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
106
+ </div>
107
+ <div class="doc-accordion-list">
108
+ ${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
109
+ </div>
110
+ </section>`;
111
+ }
112
+
113
+ function orderedTaskDocuments(task) {
114
+ const docs = taskDocTabs
115
+ .map(([key, file]) => {
116
+ const doc = taskDocument(task, file);
117
+ if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
118
+ if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
119
+ return null;
120
+ })
121
+ .filter(Boolean);
122
+ const priority = taskDocumentPriority(task);
123
+ const rank = new Map(priority.map((key, index) => [key, index]));
124
+ return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
125
+ }
126
+
127
+ function taskDocumentPriority(task) {
128
+ const stateName = task?.state || "";
129
+ const lifecycle = task?.lifecycleState || "";
130
+ if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
131
+ return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
132
+ }
133
+ if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
134
+ return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
135
+ }
136
+ if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
137
+ return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
138
+ }
139
+ return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
140
+ }
141
+
142
+ function defaultTaskDocumentKey(task, docs) {
143
+ const priority = taskDocumentPriority(task);
144
+ return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
145
+ }
146
+
147
+ function documentAccordion(item, open) {
148
+ return `<details class="doc-accordion" ${open ? "open" : ""}>
149
+ <summary>
150
+ <span>${escapeHtml(item.title)}</span>
151
+ <small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
152
+ </summary>
153
+ <div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
154
+ </details>`;
155
+ }
156
+
157
+ function documentTabs(task) {
158
+ const docs = orderedTaskDocuments(task);
159
+ return `<section class="side-panel">
160
+ <h3>${t("sourceDocuments")}</h3>
161
+ ${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>`}
162
+ </section>`;
163
+ }
164
+
165
+ function selectedSourceDocument(task, tab) {
166
+ if (!tab) return "";
167
+ const match = taskDocTabs.find(([key]) => key === tab);
168
+ if (!match) return "";
169
+ const doc = taskDocument(task, match[1]);
170
+ if (!doc) return "";
171
+ return `<section class="doc-section selected-source">
172
+ <div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
173
+ <div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
174
+ </section>`;
175
+ }
176
+
177
+ function openFindings(task) {
178
+ const risks = task.risks || [];
179
+ return `<section class="side-panel">
180
+ <h3>${t("openFindings")}</h3>
181
+ ${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>`}
182
+ </section>`;
183
+ }
184
+
185
+ function reviewActionPanel(task, { mode = "summary" } = {}) {
186
+ if (!isTaskInReviewQueue(task)) return "";
187
+ const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
188
+ const confirmed = task.reviewStatus === "confirmed";
189
+ const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
190
+ const candidateStatus = task.lessonCandidateStatus || "missing";
191
+ if (mode !== "workspace") {
192
+ return `<section class="side-panel review-actions">
193
+ <h3>${t("reviewActions")}</h3>
194
+ <p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
195
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
196
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
197
+ </section>`;
198
+ }
199
+ if (!canUseWorkbenchAction("review-complete")) {
200
+ return `<section class="side-panel review-actions">
201
+ <h3>${t("reviewActions")}</h3>
202
+ <p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
203
+ </section>`;
204
+ }
205
+ if (confirmed) {
206
+ return `<section class="side-panel review-actions">
207
+ <h3>${t("reviewActions")}</h3>
208
+ <p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
209
+ </section>`;
210
+ }
211
+ const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
212
+ const queueBlocked = !taskCanBeHumanConfirmed(task);
213
+ const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
214
+ const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
215
+ return `<section class="side-panel review-actions">
216
+ <h3>${t("reviewActions")}</h3>
217
+ <p>${escapeHtml(message)}</p>
218
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
219
+ <label class="review-check">
220
+ <input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
221
+ <span>${t("reviewConfirmChecklist")}</span>
222
+ </label>
223
+ <div class="review-confirm-copy">
224
+ ${taskCopyButton(task, "review-copy-task-name")}
225
+ </div>
226
+ <input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
227
+ <button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
228
+ <div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
229
+ </section>`;
230
+ }
231
+
232
+ function isTaskInReviewQueue(task) {
233
+ return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
234
+ }
235
+
236
+ function taskCanBeHumanConfirmed(task) {
237
+ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
238
+ }
239
+
240
+ function evidenceList(task) {
241
+ const evidence = task.evidence || [];
242
+ return `<section class="side-panel">
243
+ <h3>${t("evidence")}</h3>
244
+ ${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
245
+ </section>`;
246
+ }
@@ -0,0 +1,58 @@
1
+ function modulesView(moduleId = "") {
2
+ const graph = bundle.graph || { nodes: [], edges: [] };
3
+ const explicitModules = (graph.nodes || []).filter((node) => node.type === "module");
4
+ const moduleMap = new Map(explicitModules.map((module) => [module.id.replace(/^module:/, ""), module]));
5
+ for (const task of bundle.status?.tasks || []) {
6
+ const key = taskModuleKey(task);
7
+ if (!moduleMap.has(key)) moduleMap.set(key, { id: `module:${key}`, type: "module", label: key, state: task.classificationSource || "inferred" });
8
+ }
9
+ const modules = [...moduleMap.values()];
10
+ return `<main class="stack">
11
+ <section class="module-grid">
12
+ ${modules.map((module) => moduleCard(module)).join("") || emptyState(t("noModules"))}
13
+ </section>
14
+ </main>`;
15
+ }
16
+
17
+ function moduleTaskRow(task) {
18
+ const dotClass = /fail|blocked|open/i.test(task.state) ? "state-fail" : /warn|advice|planned|missing|unknown/i.test(task.state) ? "state-warn" : "state-pass";
19
+ return `<a class="module-task-row" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}">
20
+ <div class="module-task-left">
21
+ <i class="module-task-dot ${dotClass}" title="${escapeAttr(task.state)}"></i>
22
+ <span class="module-task-title">${escapeHtml(task.title)}</span>
23
+ </div>
24
+ <span class="module-task-pct">${task.completion}%</span>
25
+ </a>`;
26
+ }
27
+
28
+ function moduleCard(module) {
29
+ const moduleKey = module.id.replace(/^module:/, "");
30
+ const tasks = (bundle.status?.tasks || []).filter((task) => taskModuleKey(task) === moduleKey);
31
+
32
+ // Inline Pagination
33
+ state.modulePages = state.modulePages || {};
34
+ const currentPage = state.modulePages[moduleKey] || 1;
35
+ const pageCount = Math.ceil(tasks.length / 8) || 1;
36
+ const visibleTasks = tasks.slice((currentPage - 1) * 8, currentPage * 8);
37
+
38
+ const brief = findDocument(`TARGET:docs/09-PLANNING/MODULES/${moduleKey}/brief.md`);
39
+
40
+ let pagerHtml = "";
41
+ if (tasks.length > 8) {
42
+ pagerHtml = `<div class="module-pager">
43
+ <button ${currentPage <= 1 ? "disabled" : ""} onclick="window.setModulePage('${escapeAttr(moduleKey)}', ${currentPage - 1})">${t("prevPage")}</button>
44
+ <span>${currentPage} / ${pageCount}</span>
45
+ <button ${currentPage >= pageCount ? "disabled" : ""} onclick="window.setModulePage('${escapeAttr(moduleKey)}', ${currentPage + 1})">${t("nextPage")}</button>
46
+ </div>`;
47
+ }
48
+
49
+ return `<article class="module-card">
50
+ <div class="card-head"><h2>${escapeHtml(module.label || moduleKey)}</h2>${tag(module.state || "unknown")}</div>
51
+ <div class="markdown">${brief ? window.HarnessMarkdown.render(brief.content, "rendered") : `<p>${t("moduleBriefMissing")}</p>`}</div>
52
+ <h3>${t("moduleTasks")} · ${tasks.length}</h3>
53
+ <div class="module-task-list">
54
+ ${visibleTasks.map(moduleTaskRow).join("") || `<p>${t("noModuleTasks")}</p>`}
55
+ </div>
56
+ ${pagerHtml}
57
+ </article>`;
58
+ }
@@ -0,0 +1,347 @@
1
+ function reviewQueue() {
2
+ ensureReviewQueueState();
3
+ const tabs = reviewQueueTabs();
4
+ const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
5
+ const baseTasks = reviewQueueBaseTasks(activeTab);
6
+ const reasonOptions = reviewReasonOptions(baseTasks);
7
+ normalizeReviewReasonFilter(reasonOptions);
8
+ const tasks = reviewFilteredTasks(baseTasks);
9
+ const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
10
+ const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
11
+ const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
12
+ return `<div class="dashboard-grid review-queue-page">
13
+ <main class="dashboard-main stack">
14
+ <section class="flow-panel">
15
+ <div class="section-head">
16
+ <div>
17
+ <p class="eyebrow">${t("review")}</p>
18
+ <h2>${t("reviewQueue")}</h2>
19
+ <p class="subtle">${t("reviewQueueSubtitle")}</p>
20
+ </div>
21
+ <span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
22
+ </div>
23
+ <div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
24
+ ${tabs.map((tab) => reviewQueueTab(tab)).join("")}
25
+ </div>
26
+ <div class="review-queue-toolbar">
27
+ <div class="input-group">
28
+ <input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
29
+ </div>
30
+ <div class="select-group">
31
+ <label>${t("reasonFilter")}</label>
32
+ <select data-review-reason-filter aria-label="${t("reasonFilter")}">
33
+ <option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
34
+ ${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
35
+ </select>
36
+ </div>
37
+ <div class="select-group">
38
+ <label>${t("sortBy")}</label>
39
+ <select data-review-sort aria-label="${t("sortBy")}">
40
+ ${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
41
+ </select>
42
+ </div>
43
+ </div>
44
+ <div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
45
+ <div class="review-queue-list">
46
+ ${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
47
+ </div>
48
+ </div>
49
+ <div class="review-queue-pager">
50
+ ${pager("review", page, pageCount)}
51
+ </div>
52
+ </section>
53
+ </main>
54
+ <aside class="dashboard-sidebar stack">
55
+ <section class="side-panel review-queue-summary">
56
+ <h3>${t("reviewQueue")}</h3>
57
+ <div class="review-queue-stats">
58
+ ${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
59
+ </div>
60
+ </section>
61
+ <section class="side-panel">
62
+ <h3>${escapeHtml(activeTab.label)}</h3>
63
+ <p>${escapeHtml(activeTab.description)}</p>
64
+ <dl class="review-queue-contract">
65
+ <div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
66
+ <div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
67
+ </dl>
68
+ </section>
69
+ </aside>
70
+ </div>`;
71
+ }
72
+
73
+ function ensureReviewQueueState() {
74
+ if (!state.reviewQueueTab) state.reviewQueueTab = "review";
75
+ if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
76
+ if (!state.reviewSort) state.reviewSort = "queue";
77
+ if (!state.reviewQueuePage) state.reviewQueuePage = 1;
78
+ }
79
+
80
+ function reviewQueueTabs() {
81
+ return [
82
+ { id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
83
+ { id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
84
+ { id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
85
+ { id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
86
+ { id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
87
+ { id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
88
+ ];
89
+ }
90
+
91
+ function reviewQueueTab(tab) {
92
+ const active = tab.id === state.reviewQueueTab;
93
+ const count = reviewQueueBaseTasks(tab).length;
94
+ return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
95
+ <span>${escapeHtml(tab.label)}</span>
96
+ <strong>${count}</strong>
97
+ </button>`;
98
+ }
99
+
100
+ function reviewSortOptions() {
101
+ return [
102
+ { id: "queue", label: t("sortQueuePriority") },
103
+ { id: "newest", label: t("sortNewest") },
104
+ { id: "oldest", label: t("sortOldest") },
105
+ { id: "id", label: t("sortTaskId") },
106
+ ];
107
+ }
108
+
109
+ function reviewQueueBaseTasks(tab) {
110
+ return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
111
+ }
112
+
113
+ function taskMatchesReviewTab(task, tab) {
114
+ const queues = reviewTaskQueues(task);
115
+ return (tab.queues || []).some((queue) => queues.includes(queue));
116
+ }
117
+
118
+ function reviewTaskQueues(task) {
119
+ return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
120
+ }
121
+
122
+ function reviewReasonOptions(tasks) {
123
+ return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
124
+ }
125
+
126
+ function normalizeReviewReasonFilter(reasonOptions) {
127
+ const current = state.reviewReasonFilter || "all";
128
+ if (current === "all") return;
129
+ if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
130
+ }
131
+
132
+ function reviewFilteredTasks(tasks) {
133
+ const query = state.query.trim().toLowerCase();
134
+ const reasonFilter = state.reviewReasonFilter || "all";
135
+ return [...tasks]
136
+ .filter((task) => {
137
+ if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
138
+ if (!query) return true;
139
+ return [
140
+ task.id,
141
+ task.shortId,
142
+ task.title,
143
+ task.module,
144
+ task.inferredModule,
145
+ task.state,
146
+ task.lifecycleState,
147
+ task.reviewStatus,
148
+ task.closeoutStatus,
149
+ ...(task.taskQueues || []),
150
+ ...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
151
+ ].some((value) => String(value || "").toLowerCase().includes(query));
152
+ })
153
+ .sort(reviewTaskSort);
154
+ }
155
+
156
+ function reviewTaskSort(left, right) {
157
+ if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
158
+ if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
159
+ if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
160
+ return reviewPriorityRank(left) - reviewPriorityRank(right)
161
+ || compareTasksByTimeForOrder(left, right, "desc")
162
+ || stableTaskLabel(left).localeCompare(stableTaskLabel(right));
163
+ }
164
+
165
+ function compareTasksByTimeForOrder(left, right, order) {
166
+ const previous = state.taskSortOrder;
167
+ state.taskSortOrder = order;
168
+ const result = compareTasksByTime(left, right);
169
+ state.taskSortOrder = previous;
170
+ return result;
171
+ }
172
+
173
+ function reviewPriorityRank(task) {
174
+ const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
175
+ const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
176
+ const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
177
+ const queues = reviewTaskQueues(task);
178
+ const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
179
+ return Math.min(reasonRank, taskQueueRank);
180
+ }
181
+
182
+ function reviewTruthyCount(tasks, key) {
183
+ return tasks.filter((task) => task[key] === true).length;
184
+ }
185
+
186
+ function reviewQueueCard(task, tab) {
187
+ const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
188
+ const reasons = task.queueReasons || [];
189
+ const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
190
+ const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
191
+ const displayId = task.shortId || taskFolderName(task) || task.id;
192
+ return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
193
+ <div class="card-header">
194
+ <span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
195
+ ${tag(task.reviewStatus || "missing")}
196
+ ${reviewTaskQueues(task).map(tag).join("")}
197
+ </div>
198
+ <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
199
+ <div class="card-meta">
200
+ <span>${tag(task.lifecycleState || "unknown")}</span>
201
+ <span>${tag(task.closeoutStatus || "missing")}</span>
202
+ <span>${openMaterial} ${t("openFindings")}</span>
203
+ <span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
204
+ <span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
205
+ </div>
206
+ <p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
207
+ ${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
208
+ ${lessonActions}
209
+ <div class="review-queue-actions">
210
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
211
+ <a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
212
+ <button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
213
+ ${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
214
+ </div>
215
+ </article>`;
216
+ }
217
+
218
+ function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
219
+ const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
220
+ if (!candidates.length) return "";
221
+ const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
222
+ const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
223
+ const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
224
+ return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
225
+ <div class="lesson-candidate-panel-head">
226
+ <div>
227
+ <p class="eyebrow">${t("lessonCandidates")}</p>
228
+ <h3>${t("lessonSedimentationActions")}</h3>
229
+ </div>
230
+ <span class="tag">${visibleCandidates.length}/${candidates.length}</span>
231
+ </div>
232
+ ${staticNote}
233
+ <div class="lesson-candidate-actions">
234
+ ${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
235
+ </div>
236
+ ${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
237
+ </section>`;
238
+ }
239
+
240
+ function lessonCandidateAction(task, candidate) {
241
+ const followUp = String(candidate.followUpTask || "").trim();
242
+ const hasFollowUp = followUp && !/^pending$/i.test(followUp);
243
+ const prompt = lessonSedimentationPrompt(task, candidate);
244
+ return `<div class="lesson-candidate-action">
245
+ <div class="lesson-candidate-main">
246
+ <strong>${escapeHtml(candidate.id)}</strong>
247
+ <span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
248
+ <small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
249
+ </div>
250
+ <span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
251
+ <div class="lesson-candidate-command-row">
252
+ ${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
253
+ <button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
254
+ <button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
255
+ </div>
256
+ </div>`;
257
+ }
258
+
259
+ function lessonSedimentationPrompt(task, candidate) {
260
+ return [
261
+ "You are executing a lesson sedimentation follow-up task.",
262
+ "",
263
+ `Source task: ${task.id}`,
264
+ `Source candidate: ${candidate.id} - ${candidate.title || ""}`,
265
+ `Candidate scope: ${candidate.scope || "unspecified"}`,
266
+ `Candidate module key: ${candidate.moduleKey || "n/a"}`,
267
+ `Detail artifact: ${candidate.detailArtifact || "not provided"}`,
268
+ `Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
269
+ `Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
270
+ `Promotion target: ${candidate.promotionTarget || "unspecified"}`,
271
+ `Conflict check: ${candidate.conflictCheck || "pending"}`,
272
+ `Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
273
+ "",
274
+ "Instructions:",
275
+ "1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
276
+ "2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
277
+ "3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
278
+ "4. Check conflicts against existing lessons and standards.",
279
+ "5. Propose the smallest diff first.",
280
+ "6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
281
+ ].join("\n");
282
+ }
283
+
284
+ function reviewReason(reason) {
285
+ return `<div class="review-reason">
286
+ <strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
287
+ <span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
288
+ </div>`;
289
+ }
290
+
291
+ function firstUsefulLine(text) {
292
+ return String(text || "")
293
+ .split(/\n+/)
294
+ .map((line) => line.trim())
295
+ .filter(Boolean)[0] || "";
296
+ }
297
+
298
+ function reviewWorkspace(route) {
299
+ const task = (bundle.status?.tasks || []).find((item) => item.id === route.id);
300
+ if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
301
+ const walkthroughDoc = taskDocument(task, "__walkthrough__");
302
+ const candidateDoc = taskDocument(task, "lesson_candidates.md");
303
+ const reviewDoc = taskDocument(task, "review.md");
304
+ const findingsDoc = taskDocument(task, "findings.md");
305
+ return `<main class="review-workspace">
306
+ <nav class="crumbs"><a href="#/review">${t("reviewQueue")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
307
+ <section class="detail-hero review-hero">
308
+ <div>
309
+ <p class="eyebrow">${t("reviewWorkspace")}</p>
310
+ <h2>${escapeHtml(task.title)}</h2>
311
+ <p>${escapeHtml(task.path)}</p>
312
+ </div>
313
+ <div class="review-hero-tags">
314
+ ${tag(task.lifecycleState || "unknown")}
315
+ ${tag(task.reviewStatus || "missing")}
316
+ ${tag(task.lessonCandidateStatus || "missing")}
317
+ </div>
318
+ </section>
319
+ <section class="review-workspace-grid">
320
+ <article class="review-workspace-main stack">
321
+ ${reviewDocPanel("walkthrough", walkthroughDoc, task.walkthroughPath)}
322
+ ${reviewDocPanel("lessonCandidates", candidateDoc, task.lessonCandidatePath)}
323
+ ${reviewDocPanel("review", reviewDoc, task.reviewPath)}
324
+ ${reviewDocPanel("findings", findingsDoc, task.findingsPath)}
325
+ </article>
326
+ <aside class="review-workspace-side stack">
327
+ ${reviewActionPanel(task, { mode: "workspace" })}
328
+ ${taskStateSummary(task)}
329
+ ${openFindings(task)}
330
+ ${evidenceList(task)}
331
+ </aside>
332
+ </section>
333
+ </main>`;
334
+ }
335
+
336
+ function reviewDocPanel(key, doc, fallbackPath = "") {
337
+ return `<section class="doc-section review-doc-panel">
338
+ <div class="section-head">
339
+ <div>
340
+ <p class="eyebrow">${escapeHtml(fallbackPath || "")}</p>
341
+ <h2>${t(key)}</h2>
342
+ </div>
343
+ ${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
344
+ </div>
345
+ <div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
346
+ </section>`;
347
+ }