coding-agent-harness 1.0.2 → 1.0.5

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 (219) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/LICENSE +661 -21
  4. package/LICENSE-EXCEPTION.md +37 -0
  5. package/README.md +244 -87
  6. package/README.zh-CN.md +77 -35
  7. package/SKILL.md +32 -24
  8. package/docs-release/README.md +9 -5
  9. package/docs-release/architecture/overview.md +17 -5
  10. package/docs-release/architecture/overview.zh-CN.md +9 -5
  11. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  12. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  13. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  14. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  15. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  16. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  17. package/docs-release/architecture/system-explainer/README.md +67 -0
  18. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  19. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  20. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  21. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  22. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  23. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  24. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  25. package/docs-release/assets/dashboard-overview.png +0 -0
  26. package/docs-release/guides/agent-installation.en-US.md +39 -15
  27. package/docs-release/guides/agent-installation.md +43 -16
  28. package/docs-release/guides/contributing.md +100 -0
  29. package/docs-release/guides/contributing.zh-CN.md +99 -0
  30. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  31. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  32. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  33. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  34. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  35. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  36. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  37. package/docs-release/guides/migration-playbook.md +14 -15
  38. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  39. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  40. package/docs-release/guides/preset-development.md +238 -0
  41. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  42. package/docs-release/guides/repository-operating-models.md +5 -4
  43. package/docs-release/guides/task-state-machine.en-US.md +224 -0
  44. package/docs-release/guides/task-state-machine.md +231 -0
  45. package/docs-release/intl/en-US.md +1 -1
  46. package/docs-release/intl/zh-CN.md +1 -1
  47. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  48. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  49. package/package.json +10 -4
  50. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  51. package/presets/legacy-migration/preset.yaml +134 -0
  52. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  53. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  54. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  55. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  56. package/presets/legacy-migration/templates/review.seed.md +12 -0
  57. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  58. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  59. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  60. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  61. package/presets/lesson-sedimentation/preset.yaml +23 -0
  62. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  63. package/presets/module/preset.yaml +25 -0
  64. package/presets/module/templates/execution_strategy.append.md +8 -0
  65. package/presets/module/templates/task_plan.append.md +17 -0
  66. package/presets/standard-task/preset.yaml +31 -0
  67. package/presets/standard-task/templates/task_plan.append.md +7 -0
  68. package/references/adversarial-review-standard.md +2 -2
  69. package/references/agents-md-pattern.md +2 -2
  70. package/references/delivery-operating-model-standard.md +3 -3
  71. package/references/docs-directory-standard.md +6 -7
  72. package/references/harness-ledger.md +53 -96
  73. package/references/lessons-governance.md +88 -93
  74. package/references/module-parallel-standard.md +14 -14
  75. package/references/planning-loop.md +12 -6
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +11 -2
  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/walkthrough-closeout.md +7 -7
  82. package/scripts/check-harness.mjs +40 -301
  83. package/scripts/commands/dashboard-command.mjs +67 -0
  84. package/scripts/commands/migration-command.mjs +126 -0
  85. package/scripts/commands/preset-command.mjs +73 -0
  86. package/scripts/commands/task-command.mjs +328 -0
  87. package/scripts/harness.mjs +59 -260
  88. package/scripts/lib/capability-registry.mjs +82 -28
  89. package/scripts/lib/check-module-parallel.mjs +230 -0
  90. package/scripts/lib/check-profiles.mjs +90 -228
  91. package/scripts/lib/check-task-contracts.mjs +55 -0
  92. package/scripts/lib/core-shared.mjs +65 -2
  93. package/scripts/lib/dashboard-data.mjs +155 -24
  94. package/scripts/lib/dashboard-workbench.mjs +131 -12
  95. package/scripts/lib/dashboard-writer.mjs +20 -4
  96. package/scripts/lib/git-status-summary.mjs +46 -0
  97. package/scripts/lib/governance-index-generator.mjs +174 -0
  98. package/scripts/lib/governance-sync.mjs +611 -0
  99. package/scripts/lib/governance-table-boundary.mjs +175 -0
  100. package/scripts/lib/harness-core.mjs +6 -0
  101. package/scripts/lib/lesson-maintenance.mjs +36 -29
  102. package/scripts/lib/markdown-utils.mjs +33 -0
  103. package/scripts/lib/migration-planner.mjs +4 -6
  104. package/scripts/lib/migration-support.mjs +1 -1
  105. package/scripts/lib/phase-kind.mjs +50 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +494 -0
  108. package/scripts/lib/preset-registry.mjs +776 -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-builder.mjs +88 -0
  112. package/scripts/lib/status-dashboard-renderer.mjs +105 -0
  113. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  114. package/scripts/lib/task-audit-metadata.mjs +385 -0
  115. package/scripts/lib/task-audit-migration.mjs +350 -0
  116. package/scripts/lib/task-completion-consistency.mjs +26 -0
  117. package/scripts/lib/task-index.mjs +93 -0
  118. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  119. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  120. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  121. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  122. package/scripts/lib/task-lifecycle/review-confirm.mjs +112 -0
  123. package/scripts/lib/task-lifecycle/review-gates.mjs +73 -0
  124. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  125. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  126. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  127. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  128. package/scripts/lib/task-lifecycle.mjs +338 -477
  129. package/scripts/lib/task-metadata.mjs +118 -0
  130. package/scripts/lib/task-review-model.mjs +455 -0
  131. package/scripts/lib/task-scanner.mjs +193 -372
  132. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  133. package/scripts/postinstall.mjs +14 -0
  134. package/skills/preset-creator/SKILL.md +179 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  137. package/skills/preset-creator/references/complex-task-skeleton/brief.md +43 -0
  138. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  139. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  140. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  141. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  142. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  143. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  144. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  145. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  146. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  147. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  148. package/templates/AGENTS.md.template +24 -18
  149. package/templates/dashboard/assets/app-src/00-state.js +13 -0
  150. package/templates/dashboard/assets/app-src/10-router.js +5 -1
  151. package/templates/dashboard/assets/app-src/20-overview.js +18 -8
  152. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  153. package/templates/dashboard/assets/app-src/35-task-detail.js +286 -0
  154. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  155. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  156. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  157. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  158. package/templates/dashboard/assets/app-src/90-bindings.js +302 -29
  159. package/templates/dashboard/assets/app.css +1501 -376
  160. package/templates/dashboard/assets/app.css.manifest.json +10 -0
  161. package/templates/dashboard/assets/app.js +1240 -101
  162. package/templates/dashboard/assets/app.manifest.json +2 -0
  163. package/templates/dashboard/assets/css-src/00-foundation.css +346 -0
  164. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  165. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  166. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  167. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  168. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +489 -0
  169. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  170. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  171. package/templates/dashboard/assets/i18n.js +263 -23
  172. package/templates/ledger/Harness-Ledger.md +13 -25
  173. package/templates/lessons/lesson-arch-process-change.md +1 -1
  174. package/templates/lessons/lesson-new-doc.md +1 -1
  175. package/templates/lessons/lesson-ref-change.md +1 -1
  176. package/templates/planning/INDEX.md +87 -0
  177. package/templates/planning/brief.md +1 -1
  178. package/templates/planning/execution_strategy.md +31 -0
  179. package/templates/planning/lesson_candidates.md +18 -6
  180. package/templates/planning/module_session_prompt.md +1 -0
  181. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  182. package/templates/planning/optional/references/INDEX.md +3 -3
  183. package/templates/planning/review.md +41 -0
  184. package/templates/planning/task_plan.md +5 -21
  185. package/templates/planning/visual_map.md +13 -9
  186. package/templates/planning/visual_map.simple.md +52 -0
  187. package/templates/reference/execution-workflow-standard.md +31 -3
  188. package/templates/reference/pull-request-standard.md +80 -0
  189. package/templates/reference/repo-governance-standard.md +7 -6
  190. package/templates/reference/review-routing-standard.md +6 -0
  191. package/templates/reference/walkthrough-standard.md +2 -1
  192. package/templates/verifier/verifier-output.md +1 -1
  193. package/templates-zh-CN/AGENTS.md.template +25 -19
  194. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  195. package/templates-zh-CN/planning/INDEX.md +87 -0
  196. package/templates-zh-CN/planning/brief.md +1 -1
  197. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  198. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  199. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  200. package/templates-zh-CN/planning/review.md +41 -1
  201. package/templates-zh-CN/planning/task_plan.md +4 -44
  202. package/templates-zh-CN/planning/visual_map.md +14 -7
  203. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  204. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  205. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  206. package/templates-zh-CN/reference/execution-workflow-standard.md +33 -7
  207. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  208. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  209. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  210. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  211. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  212. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  213. package/docs-release/assets/dashboard-overview-en.png +0 -0
  214. package/scripts/smoke-dashboard.mjs +0 -92
  215. package/scripts/test-harness.mjs +0 -1395
  216. package/templates/ssot/Feature-SSoT.md +0 -43
  217. package/templates/ssot/Lessons-SSoT.md +0 -44
  218. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  219. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -0,0 +1,286 @@
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
+ const knownKinds = new Set(["init", "execution", "gate"]);
76
+ const groups = [
77
+ ["init", "Init"],
78
+ ["execution", "Execution"],
79
+ ["gate", "Gate"],
80
+ ["other", "Other / Invalid"],
81
+ ];
82
+ const phases = task.phases || [];
83
+ const grouped = groups
84
+ .map(([kind, label]) => {
85
+ const items = kind === "other"
86
+ ? phases.filter((phase) => !knownKinds.has(phase.kind || "execution"))
87
+ : phases.filter((phase) => (phase.kind || "execution") === kind);
88
+ if (!items.length) return "";
89
+ return `<div class="phase-kind-group ${escapeAttr(kind)}">
90
+ <h3>${escapeHtml(label)}</h3>
91
+ ${items.map(phaseStep).join("")}
92
+ </div>`;
93
+ })
94
+ .join("");
95
+ return `<section class="phase-timeline">
96
+ <h2>${t("phaseTimeline")}</h2>
97
+ ${grouped || emptyState(t("noPhaseData"))}
98
+ </section>`;
99
+ }
100
+
101
+ function phaseStep(phase) {
102
+ const kind = phase.kind || "execution";
103
+ const actor = phase.actor || "agent";
104
+ const knownKind = ["init", "execution", "gate"].includes(kind);
105
+ const kindLabel = knownKind ? escapeHtml(kind) : `<span class="tag warn">${escapeHtml(kind)}</span>`;
106
+ const phaseKindClass = knownKind ? kind : "other";
107
+ return `<div class="phase-step ${escapeAttr(phase.state)} ${escapeAttr(phaseKindClass)}">
108
+ <div class="phase-step-head">
109
+ <strong>${escapeHtml(phase.id)}</strong>
110
+ <span>${kindLabel} · ${phase.completion}%</span>
111
+ </div>
112
+ <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
113
+ ${progressBar(phase.completion)}
114
+ <div class="phase-meta">
115
+ ${phaseMetaTag(actor)}
116
+ ${tag(phase.evidenceStatus || "missing")}
117
+ </div>
118
+ ${phase.exitCommand ? `<code class="phase-exit-command">${escapeHtml(phase.exitCommand)}</code>` : ""}
119
+ </div>`;
120
+ }
121
+
122
+ function phaseMetaTag(value) {
123
+ return `<span class="tag">${escapeHtml(String(value || "unknown").replaceAll("_", " "))}</span>`;
124
+ }
125
+
126
+ function taskDocSection(task, fileName, title, required) {
127
+ const doc = taskDocument(task, fileName);
128
+ if (!doc && !required) return "";
129
+ return `<section class="doc-section">
130
+ <div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
131
+ <div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
132
+ </section>`;
133
+ }
134
+
135
+ function taskDocumentLibrary(task, selectedTab) {
136
+ const docs = orderedTaskDocuments(task);
137
+ if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
138
+ const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
139
+ return `<section class="doc-library">
140
+ <div class="section-head">
141
+ <div>
142
+ <p class="eyebrow">${t("taskDocuments")}</p>
143
+ <h2>${escapeHtml(t("sourceDocuments"))}</h2>
144
+ </div>
145
+ <button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
146
+ </div>
147
+ <div class="doc-accordion-list">
148
+ ${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
149
+ </div>
150
+ </section>`;
151
+ }
152
+
153
+ function orderedTaskDocuments(task) {
154
+ const docs = taskDocTabs
155
+ .map(([key, file]) => {
156
+ const doc = taskDocument(task, file);
157
+ if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
158
+ if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
159
+ return null;
160
+ })
161
+ .filter(Boolean);
162
+ const priority = taskDocumentPriority(task);
163
+ const rank = new Map(priority.map((key, index) => [key, index]));
164
+ return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
165
+ }
166
+
167
+ function taskDocumentPriority(task) {
168
+ const stateName = task?.state || "";
169
+ const lifecycle = task?.lifecycleState || "";
170
+ if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
171
+ return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
172
+ }
173
+ if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
174
+ return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
175
+ }
176
+ if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
177
+ return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
178
+ }
179
+ return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
180
+ }
181
+
182
+ function defaultTaskDocumentKey(task, docs) {
183
+ const priority = taskDocumentPriority(task);
184
+ return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
185
+ }
186
+
187
+ function documentAccordion(item, open) {
188
+ return `<details class="doc-accordion" ${open ? "open" : ""}>
189
+ <summary>
190
+ <span>${escapeHtml(item.title)}</span>
191
+ <small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
192
+ </summary>
193
+ <div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
194
+ </details>`;
195
+ }
196
+
197
+ function documentTabs(task) {
198
+ const docs = orderedTaskDocuments(task);
199
+ return `<section class="side-panel">
200
+ <h3>${t("sourceDocuments")}</h3>
201
+ ${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>`}
202
+ </section>`;
203
+ }
204
+
205
+ function selectedSourceDocument(task, tab) {
206
+ if (!tab) return "";
207
+ const match = taskDocTabs.find(([key]) => key === tab);
208
+ if (!match) return "";
209
+ const doc = taskDocument(task, match[1]);
210
+ if (!doc) return "";
211
+ return `<section class="doc-section selected-source">
212
+ <div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
213
+ <div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
214
+ </section>`;
215
+ }
216
+
217
+ function openFindings(task) {
218
+ const risks = task.risks || [];
219
+ return `<section class="side-panel">
220
+ <h3>${t("openFindings")}</h3>
221
+ ${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>`}
222
+ </section>`;
223
+ }
224
+
225
+ function reviewActionPanel(task, { mode = "summary" } = {}) {
226
+ if (!isTaskInReviewQueue(task)) return "";
227
+ const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
228
+ const confirmed = task.reviewStatus === "confirmed";
229
+ const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
230
+ const candidateStatus = task.lessonCandidateStatus || "missing";
231
+ if (mode !== "workspace") {
232
+ return `<section class="side-panel review-actions">
233
+ <h3>${t("reviewActions")}</h3>
234
+ <p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
235
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
236
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
237
+ </section>`;
238
+ }
239
+ if (!canUseWorkbenchAction("review-complete")) {
240
+ return `<section class="side-panel review-actions">
241
+ <h3>${t("reviewActions")}</h3>
242
+ <p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
243
+ </section>`;
244
+ }
245
+ if (confirmed) {
246
+ return `<section class="side-panel review-actions">
247
+ <h3>${t("reviewActions")}</h3>
248
+ <p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
249
+ </section>`;
250
+ }
251
+ const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
252
+ const queueBlocked = !taskCanBeHumanConfirmed(task);
253
+ const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
254
+ const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
255
+ return `<section class="side-panel review-actions">
256
+ <h3>${t("reviewActions")}</h3>
257
+ <p>${escapeHtml(message)}</p>
258
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
259
+ <label class="review-check">
260
+ <input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
261
+ <span>${t("reviewConfirmChecklist")}</span>
262
+ </label>
263
+ <div class="review-confirm-copy">
264
+ ${taskCopyButton(task, "review-copy-task-name")}
265
+ </div>
266
+ <input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
267
+ <button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
268
+ <div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
269
+ </section>`;
270
+ }
271
+
272
+ function isTaskInReviewQueue(task) {
273
+ return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
274
+ }
275
+
276
+ function taskCanBeHumanConfirmed(task) {
277
+ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
278
+ }
279
+
280
+ function evidenceList(task) {
281
+ const evidence = task.evidence || [];
282
+ return `<section class="side-panel">
283
+ <h3>${t("evidence")}</h3>
284
+ ${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
285
+ </section>`;
286
+ }
@@ -1,8 +1,14 @@
1
1
  function reviewQueue() {
2
- const tasks = reviewQueueTasks();
3
- const ready = tasks.filter((task) => task.reviewStatus !== "blocked-open-findings" && task.reviewStatus !== "confirmed").length;
4
- const blocked = tasks.filter((task) => task.reviewStatus === "blocked-open-findings").length;
5
- const confirmed = tasks.filter((task) => task.reviewStatus === "confirmed").length;
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);
6
12
  return `<div class="dashboard-grid review-queue-page">
7
13
  <main class="dashboard-main stack">
8
14
  <section class="flow-panel">
@@ -12,10 +18,36 @@ function reviewQueue() {
12
18
  <h2>${t("reviewQueue")}</h2>
13
19
  <p class="subtle">${t("reviewQueueSubtitle")}</p>
14
20
  </div>
15
- <span class="subtle">${ready}/${tasks.length} ${t("reviewReady")}</span>
21
+ <span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
16
22
  </div>
17
- <div class="task-card-grid review-queue-grid">
18
- ${tasks.map(reviewQueueCard).join("") || emptyState(t("noReviewTasks"))}
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)}
19
51
  </div>
20
52
  </section>
21
53
  </main>
@@ -23,52 +55,239 @@ function reviewQueue() {
23
55
  <section class="side-panel review-queue-summary">
24
56
  <h3>${t("reviewQueue")}</h3>
25
57
  <div class="review-queue-stats">
26
- ${metric(t("reviewReady"), ready)}
27
- ${metric(t("reviewBlockedQueue"), blocked)}
28
- ${metric(t("reviewConfirmedQueue"), confirmed)}
58
+ ${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
29
59
  </div>
30
60
  </section>
31
61
  <section class="side-panel">
32
- <h3>${t("review")}</h3>
33
- <p>${escapeHtml(t("reviewQueueSubtitle"))}</p>
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>
34
68
  </section>
35
69
  </aside>
36
70
  </div>`;
37
71
  }
38
72
 
39
- function reviewQueueTasks() {
40
- return (bundle.status?.tasks || [])
41
- .filter(isTaskInReviewStage)
42
- .sort((left, right) => reviewSortKey(left).localeCompare(reviewSortKey(right)));
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);
43
180
  }
44
181
 
45
- function reviewSortKey(task) {
46
- const rank = task.reviewStatus === "blocked-open-findings" ? "0" : task.reviewStatus === "confirmed" ? "2" : "1";
47
- return `${rank}:${task.id}`;
182
+ function reviewTruthyCount(tasks, key) {
183
+ return tasks.filter((task) => task[key] === true).length;
48
184
  }
49
185
 
50
- function reviewQueueCard(task) {
186
+ function reviewQueueCard(task, tab) {
51
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;
52
192
  return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
53
193
  <div class="card-header">
54
- <span class="card-id">${escapeHtml(task.id)}</span>
194
+ <span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
55
195
  ${tag(task.reviewStatus || "missing")}
196
+ ${reviewTaskQueues(task).map(tag).join("")}
56
197
  </div>
57
198
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
58
199
  <div class="card-meta">
59
200
  <span>${tag(task.lifecycleState || "unknown")}</span>
60
201
  <span>${tag(task.closeoutStatus || "missing")}</span>
61
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>
62
205
  </div>
63
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}
64
209
  <div class="review-queue-actions">
65
210
  <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
66
211
  <a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
67
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>` : ""}
68
214
  </div>
69
215
  </article>`;
70
216
  }
71
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
+
72
291
  function firstUsefulLine(text) {
73
292
  return String(text || "")
74
293
  .split(/\n+/)
@@ -123,6 +342,6 @@ function reviewDocPanel(key, doc, fallbackPath = "") {
123
342
  </div>
124
343
  ${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
125
344
  </div>
126
- <div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div>
345
+ <div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
127
346
  </section>`;
128
347
  }
@@ -138,25 +138,39 @@ function pager(kind, page, pageCount, group = "") {
138
138
  }
139
139
 
140
140
  function lessonPanel() {
141
- const lessons = (bundle.tables?.tables || [])
142
- .filter((table) => table.kind === "lessons-ssot")
143
- .flatMap((table) => table.rows);
141
+ const lessons = lessonDocuments();
144
142
  return `<section class="lesson-panel">
145
143
  <div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
146
144
  <div class="lesson-list" style="padding-top: 10px;">
147
- ${lessons.map((row) => {
148
- const cells = row.cells || {};
149
- const lessonId = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
150
- const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
151
- return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lessonId)}">
152
- <strong>${escapeHtml(lessonId)}</strong>
153
- <p>${escapeHtml(summary)}</p>
145
+ ${lessons.map((lesson) => {
146
+ return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
147
+ <strong>${escapeHtml(lesson.id)}</strong>
148
+ <p>${escapeHtml(lesson.title || lesson.path)}</p>
154
149
  </div>`;
155
150
  }).join("") || emptyState(t("noLessons"))}
156
151
  </div>
157
152
  </section>`;
158
153
  }
159
154
 
155
+ function lessonDocuments() {
156
+ return (bundle.documents?.documents || [])
157
+ .filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
158
+ .map((doc) => {
159
+ const id = lessonIdFromDocument(doc);
160
+ return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
161
+ })
162
+ .filter((lesson) => lesson.id)
163
+ .sort((left, right) => String(right.id).localeCompare(String(left.id)));
164
+ }
165
+
166
+ function lessonIdFromDocument(doc) {
167
+ const content = doc?.content || "";
168
+ const path = doc?.path || "";
169
+ return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
170
+ || path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
171
+ || "";
172
+ }
173
+
160
174
  function healthPanel() {
161
175
  const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
162
176
  return `<section class="health-panel">