coding-agent-harness 1.0.8 → 1.1.0

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 (232) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CONTRIBUTING.md +8 -4
  3. package/README.md +12 -2
  4. package/README.zh-CN.md +10 -2
  5. package/SKILL.md +14 -3
  6. package/dist/build-dist.mjs +19 -6
  7. package/dist/check-dist-observation.mjs +57 -29
  8. package/dist/check-harness.mjs +0 -1
  9. package/dist/check-import-graph.mjs +44 -27
  10. package/dist/check-lite-forbidden-surfaces.mjs +121 -0
  11. package/dist/check-no-ts-nocheck.mjs +7 -7
  12. package/dist/check-runtime-emit.mjs +10 -3
  13. package/dist/check-type-boundaries.mjs +51 -9
  14. package/dist/commands/dashboard-command.mjs +52 -14
  15. package/dist/commands/migration-command.mjs +18 -8
  16. package/dist/commands/module-command.mjs +142 -0
  17. package/dist/commands/preset-command.mjs +51 -12
  18. package/dist/commands/registry.mjs +483 -0
  19. package/dist/commands/task-command.mjs +109 -52
  20. package/dist/harness.mjs +6 -304
  21. package/dist/lib/capability-registry.mjs +229 -53
  22. package/dist/lib/check-module-parallel.mjs +1 -6
  23. package/dist/lib/check-profiles.mjs +39 -46
  24. package/dist/lib/check-task-contracts.mjs +6 -4
  25. package/dist/lib/command-registry.mjs +248 -0
  26. package/dist/lib/core-shared.mjs +78 -3
  27. package/dist/lib/dashboard-data.mjs +203 -22
  28. package/dist/lib/dashboard-workbench.mjs +245 -21
  29. package/dist/lib/dashboard-writer.mjs +4 -1
  30. package/dist/lib/git-status-summary.mjs +0 -1
  31. package/dist/lib/governance-index-generator.mjs +7 -5
  32. package/dist/lib/governance-sync.mjs +46 -121
  33. package/dist/lib/governance-table-boundary.mjs +1 -14
  34. package/dist/lib/harness-core.mjs +4 -1
  35. package/dist/lib/harness-paths.mjs +115 -1
  36. package/dist/lib/impact-classifier.mjs +420 -0
  37. package/dist/lib/lesson-maintenance.mjs +1 -2
  38. package/dist/lib/markdown-utils.mjs +50 -1
  39. package/dist/lib/migration-planner.mjs +31 -16
  40. package/dist/lib/migration-support.mjs +5 -4
  41. package/dist/lib/module-registry.mjs +296 -0
  42. package/dist/lib/preset-audit-contracts.mjs +24 -1
  43. package/dist/lib/preset-engine.mjs +67 -29
  44. package/dist/lib/preset-registry.mjs +361 -71
  45. package/dist/lib/preset-runner.mjs +292 -26
  46. package/dist/lib/review-confirm-git-gate.mjs +73 -19
  47. package/dist/lib/status-builder.mjs +23 -8
  48. package/dist/lib/structure-migration.mjs +6 -4
  49. package/dist/lib/subagent-authorization-audit.mjs +8 -2
  50. package/dist/lib/task-archive-eligibility.mjs +65 -0
  51. package/dist/lib/task-audit-metadata.mjs +25 -11
  52. package/dist/lib/task-audit-migration.mjs +21 -14
  53. package/dist/lib/task-discovery-contract.mjs +32 -0
  54. package/dist/lib/task-index.mjs +3 -2
  55. package/dist/lib/task-lesson-candidates.mjs +1 -2
  56. package/dist/lib/task-lesson-sedimentation.mjs +310 -9
  57. package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
  58. package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
  59. package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
  60. package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
  61. package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
  62. package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
  63. package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
  64. package/dist/lib/task-lifecycle/template-files.mjs +2 -5
  65. package/dist/lib/task-lifecycle.mjs +116 -160
  66. package/dist/lib/task-metadata.mjs +10 -5
  67. package/dist/lib/task-preset-contract-drift.mjs +45 -0
  68. package/dist/lib/task-repository.mjs +192 -0
  69. package/dist/lib/task-review-model.mjs +36 -17
  70. package/dist/lib/task-scanner.mjs +74 -23
  71. package/dist/lib/task-template-materials.mjs +131 -0
  72. package/dist/lib/task-tombstone-commands.mjs +186 -29
  73. package/dist/lib/types/check-profiles.js +1 -0
  74. package/dist/lib/types/impact.js +1 -0
  75. package/dist/lib/types/preset.js +1 -0
  76. package/dist/lib/types/task-lifecycle.js +1 -0
  77. package/dist/lib/types/task-scanner.js +1 -0
  78. package/dist/postinstall.mjs +2 -2
  79. package/dist/run-built-tests.mjs +10 -3
  80. package/docs-release/README.md +1 -0
  81. package/docs-release/architecture/document-contract-kernel/README.md +150 -0
  82. package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
  83. package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
  84. package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
  85. package/docs-release/architecture/overview.md +2 -2
  86. package/docs-release/architecture/overview.zh-CN.md +2 -2
  87. package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
  88. package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
  89. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
  90. package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
  91. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
  92. package/docs-release/architecture/system-explainer/README.md +1 -1
  93. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
  94. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
  95. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
  96. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
  97. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
  98. package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
  99. package/docs-release/guides/agent-installation.en-US.md +4 -6
  100. package/docs-release/guides/agent-installation.md +11 -8
  101. package/docs-release/guides/contributing.md +10 -3
  102. package/docs-release/guides/contributing.zh-CN.md +10 -3
  103. package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
  104. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
  105. package/docs-release/guides/migration-playbook.en-US.md +9 -6
  106. package/docs-release/guides/migration-playbook.md +9 -6
  107. package/docs-release/guides/preset-development.md +68 -2
  108. package/docs-release/guides/task-state-machine.en-US.md +8 -8
  109. package/docs-release/guides/task-state-machine.md +7 -7
  110. package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
  111. package/package.json +16 -12
  112. package/postinstall.mjs +37 -0
  113. package/presets/legacy-migration/preset.yaml +5 -5
  114. package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
  115. package/presets/lesson-sedimentation/preset.yaml +3 -3
  116. package/presets/module/preset.yaml +2 -2
  117. package/presets/module/templates/execution_strategy.append.md +1 -1
  118. package/presets/module/templates/task_plan.append.md +3 -3
  119. package/presets/release-closeout/checks/check-release-package.mjs +6 -1
  120. package/presets/release-closeout/preset.yaml +9 -9
  121. package/presets/release-closeout/scripts/generate-release-package.mjs +387 -25
  122. package/presets/release-closeout/templates/task_plan.append.md +5 -5
  123. package/presets/standard-task/preset.yaml +2 -2
  124. package/references/agents-md-pattern.md +23 -17
  125. package/references/lessons-governance.md +2 -2
  126. package/references/module-parallel-standard.md +3 -6
  127. package/references/ssot-governance.md +2 -2
  128. package/references/taskr-gap-analysis.md +3 -3
  129. package/run-dist.mjs +34 -0
  130. package/skills/preset-creator/SKILL.md +40 -8
  131. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
  132. package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
  133. package/skills/preset-creator/references/structure-aware-paths.md +112 -0
  134. package/templates/AGENTS.md.template +28 -26
  135. package/templates/architecture/README.md +2 -2
  136. package/templates/architecture/service-catalog.md +2 -2
  137. package/templates/architecture/services/service-template.md +1 -1
  138. package/templates/dashboard/assets/app-src/00-state.js +5 -1
  139. package/templates/dashboard/assets/app-src/10-router.js +7 -0
  140. package/templates/dashboard/assets/app-src/20-overview.js +8 -8
  141. package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
  142. package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
  143. package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
  144. package/templates/dashboard/assets/app-src/40-modules.js +257 -41
  145. package/templates/dashboard/assets/app-src/45-review.js +127 -1
  146. package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
  147. package/templates/dashboard/assets/app.css +928 -53
  148. package/templates/dashboard/assets/app.css.manifest.json +2 -0
  149. package/templates/dashboard/assets/app.js +1071 -98
  150. package/templates/dashboard/assets/app.manifest.json +1 -0
  151. package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
  152. package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
  153. package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
  154. package/templates/dashboard/assets/css-src/31-archive.css +94 -0
  155. package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
  156. package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
  157. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
  158. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
  159. package/templates/dashboard/assets/i18n.js +166 -2
  160. package/templates/development/README.md +9 -9
  161. package/templates/development/cross-repo-debugging.md +3 -3
  162. package/templates/development/external-context/service-template.md +1 -1
  163. package/templates/development/external-source-packs/README.md +2 -2
  164. package/templates/integrations/README.md +4 -4
  165. package/templates/integrations/api-contract.md +1 -1
  166. package/templates/integrations/event-contract.md +1 -1
  167. package/templates/integrations/third-party/vendor-template.md +1 -1
  168. package/templates/integrations/webhook-contract.md +1 -1
  169. package/templates/ledger/Harness-Ledger.md +1 -1
  170. package/templates/modules/module_brief.md +50 -0
  171. package/templates/modules/module_plan.md +49 -0
  172. package/templates/modules/registry_view.md +9 -0
  173. package/templates/modules/session_prompt_pack.md +55 -0
  174. package/templates/planning/brief.md +32 -8
  175. package/templates/planning/module_brief.md +28 -3
  176. package/templates/planning/module_plan.md +26 -11
  177. package/templates/planning/module_session_prompt.md +11 -2
  178. package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
  179. package/templates/planning/review.md +1 -1
  180. package/templates/planning/visual_map.md +1 -1
  181. package/templates/reference/docs-library-standard.md +7 -7
  182. package/templates/reference/execution-workflow-standard.md +13 -0
  183. package/templates/reference/external-source-intake-standard.md +10 -10
  184. package/templates/reference/repo-governance-standard.md +1 -1
  185. package/templates/reference/review-routing-standard.md +4 -0
  186. package/templates/ssot/Module-Registry.md +4 -38
  187. package/templates/walkthrough/walkthrough-template.md +1 -1
  188. package/templates-zh-CN/AGENTS.md.template +27 -25
  189. package/templates-zh-CN/CLAUDE.md.template +1 -1
  190. package/templates-zh-CN/architecture/README.md +2 -2
  191. package/templates-zh-CN/architecture/service-catalog.md +2 -2
  192. package/templates-zh-CN/architecture/services/service-template.md +1 -1
  193. package/templates-zh-CN/development/README.md +9 -9
  194. package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
  195. package/templates-zh-CN/development/external-context/service-template.md +1 -1
  196. package/templates-zh-CN/development/external-source-packs/README.md +2 -2
  197. package/templates-zh-CN/integrations/README.md +4 -4
  198. package/templates-zh-CN/integrations/api-contract.md +1 -1
  199. package/templates-zh-CN/integrations/event-contract.md +1 -1
  200. package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
  201. package/templates-zh-CN/integrations/webhook-contract.md +1 -1
  202. package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
  203. package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
  204. package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
  205. package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
  206. package/templates-zh-CN/modules/module_brief.md +47 -0
  207. package/templates-zh-CN/modules/module_plan.md +48 -0
  208. package/templates-zh-CN/modules/registry_view.md +9 -0
  209. package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
  210. package/templates-zh-CN/planning/INDEX.md +1 -0
  211. package/templates-zh-CN/planning/brief.md +26 -7
  212. package/templates-zh-CN/planning/module_brief.md +24 -2
  213. package/templates-zh-CN/planning/module_plan.md +35 -29
  214. package/templates-zh-CN/planning/module_session_prompt.md +15 -11
  215. package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
  216. package/templates-zh-CN/planning/review.md +1 -1
  217. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  218. package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
  219. package/templates-zh-CN/reference/docs-library-standard.md +27 -27
  220. package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
  221. package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
  222. package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
  223. package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
  224. package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
  225. package/templates-zh-CN/reference/review-routing-standard.md +3 -0
  226. package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
  227. package/templates-zh-CN/reference/worktree-standard.md +1 -1
  228. package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
  229. package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
  230. package/templates-zh-CN/ssot/Module-Registry.md +5 -44
  231. package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
  232. package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
@@ -1,58 +1,274 @@
1
+ function dashboardModules() {
2
+ const structured = Array.isArray(bundle.modules) ? bundle.modules : [];
3
+ if (structured.length > 0) return structured;
4
+ const graphModules = (bundle.graph?.nodes || []).filter((node) => node.type === "module").map((node) => ({
5
+ key: String(node.id || "").replace(/^module:/, ""),
6
+ title: node.label,
7
+ status: node.state,
8
+ currentStep: node.currentStep,
9
+ source: "graph",
10
+ briefPath: node.briefPath,
11
+ modulePlanPath: node.modulePlanPath,
12
+ })).filter((module) => module.key);
13
+ return graphModules;
14
+ }
15
+
16
+ function moduleDefinition(key) {
17
+ return dashboardModules().find((module) => module.key === key) || null;
18
+ }
19
+
20
+ function taskModuleLabel(task) {
21
+ const key = taskModuleKey(task);
22
+ if (key === "base" || key === "legacy-unclassified") return taskModuleDisplayLabel(key);
23
+ return moduleDefinition(key)?.title || key;
24
+ }
25
+
26
+ function taskGroupContext(group, tasks) {
27
+ if (group.startsWith("module:")) {
28
+ const key = group.slice("module:".length);
29
+ const counts = moduleCountsForTasks(tasks);
30
+ if (key === "base") {
31
+ return {
32
+ eyebrow: t("baseModuleEyebrow"),
33
+ title: t("baseModule"),
34
+ summary: `${tasks.length} ${t("tasks")} · ${counts.active} ${t("active")} · ${counts.review} ${t("statReview")} · ${counts.blocked} ${t("statBlocked")}`,
35
+ chips: [`${tasks.length} ${t("tasks")}`, `${counts.risk} ${t("moduleRisks")}`],
36
+ };
37
+ }
38
+ if (key === "legacy-unclassified") {
39
+ return {
40
+ eyebrow: t("unclassifiedWarning"),
41
+ title: t("unclassifiedModule"),
42
+ summary: t("unclassifiedSummary").replace("{count}", String(tasks.length)),
43
+ chips: [`${tasks.length} ${t("tasks")}`, `${counts.risk} ${t("moduleRisks")}`],
44
+ };
45
+ }
46
+ const module = moduleDefinition(key) || { key, title: key, source: "inferred" };
47
+ const chips = [
48
+ module.status ? `${t("columnState")}: ${label(module.status)}` : "",
49
+ module.owner ? `${t("moduleOwner")}: ${module.owner}` : "",
50
+ module.currentStep ? `${t("moduleCurrentStep")}: ${module.currentStep}` : "",
51
+ module.dependsOn?.length ? `${t("moduleDependsOn")}: ${module.dependsOn.join(", ")}` : "",
52
+ module.scope?.length ? `${t("moduleScope")}: ${module.scope.join(", ")}` : "",
53
+ ].filter(Boolean);
54
+ return {
55
+ eyebrow: module.source === "registry" ? t("registeredModule") : t("inferredModule"),
56
+ title: module.title || key,
57
+ summary: `${tasks.length} ${t("tasks")} · ${counts.active} ${t("active")} · ${counts.review} ${t("statReview")} · ${counts.blocked} ${t("statBlocked")}`,
58
+ chips,
59
+ };
60
+ }
61
+ return {
62
+ eyebrow: t("groupBy"),
63
+ title: taskGroupLabel(group),
64
+ summary: `${tasks.length} ${t("tasks")}`,
65
+ chips: [],
66
+ };
67
+ }
68
+
69
+ function moduleCountsForTasks(tasks) {
70
+ return {
71
+ active: tasks.filter((task) => ["in_progress", "review", "blocked", "planned", "not_started"].includes(task.state)).length,
72
+ review: tasks.filter((task) => task.state === "review").length,
73
+ blocked: tasks.filter((task) => task.state === "blocked").length,
74
+ risk: tasks.filter(uiDashboardTaskHasRisk).length,
75
+ };
76
+ }
77
+
1
78
  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 || []) {
79
+ const modules = modulesWithTaskFallback();
80
+ const selectedKey = moduleId || state.selectedModuleKey || modules[0]?.key || "";
81
+ state.selectedModuleKey = selectedKey;
82
+ const selected = modules.find((module) => module.key === selectedKey) || modules[0] || null;
83
+ const unclassified = normalCycleTasks().filter((task) => taskModuleKey(task) === "legacy-unclassified");
84
+ return `<main class="stack module-console">
85
+ ${moduleRunStrip(modules, unclassified)}
86
+ <section class="module-console-grid">
87
+ <nav class="module-list-panel" aria-label="${escapeAttr(t("moduleView"))}">
88
+ ${modules.map((module) => moduleListItem(module, selected?.key === module.key)).join("") || emptyState(t("noModules"))}
89
+ </nav>
90
+ <article class="module-detail-panel">
91
+ ${selected ? moduleDetail(selected) : emptyState(t("noModules"))}
92
+ </article>
93
+ </section>
94
+ ${unclassified.length ? moduleUnclassifiedPanel(unclassified) : ""}
95
+ </main>`;
96
+ }
97
+
98
+ function modulesWithTaskFallback() {
99
+ const moduleMap = new Map(dashboardModules().map((module) => [module.key, {
100
+ ...module,
101
+ counts: emptyUiModuleCounts(),
102
+ tasks: [],
103
+ }]));
104
+ for (const task of normalCycleTasks()) {
6
105
  const key = taskModuleKey(task);
7
- if (!moduleMap.has(key)) moduleMap.set(key, { id: `module:${key}`, type: "module", label: key, state: task.classificationSource || "inferred" });
106
+ if (key === "legacy-unclassified") continue;
107
+ if (!moduleMap.has(key)) {
108
+ moduleMap.set(key, {
109
+ key,
110
+ title: taskModuleDisplayLabel(key),
111
+ source: key === "base" ? "structure" : "inferred",
112
+ status: task.classificationSource || "inferred",
113
+ counts: emptyUiModuleCounts(),
114
+ tasks: [],
115
+ });
116
+ }
117
+ const module = moduleMap.get(key);
118
+ accumulateUiModuleTask(module, task);
119
+ }
120
+ return [...moduleMap.values()].sort((left, right) => {
121
+ const leftActive = Number(left.counts?.active || 0);
122
+ const rightActive = Number(right.counts?.active || 0);
123
+ if (leftActive !== rightActive) return rightActive - leftActive;
124
+ return left.key.localeCompare(right.key);
125
+ });
126
+ }
127
+
128
+ function emptyUiModuleCounts() {
129
+ return { total: 0, active: 0, review: 0, blocked: 0, risk: 0, missingDocs: 0 };
130
+ }
131
+
132
+ function accumulateUiModuleTask(module, task) {
133
+ if (!module || !task) return;
134
+ const stateValue = String(task.state || "unknown");
135
+ if (!module.tasks.some((item) => item.id === task.id)) module.tasks.push(task);
136
+ module.counts.total = (module.counts.total || 0) + 1;
137
+ if (["in_progress", "review", "blocked", "planned", "not_started"].includes(stateValue)) {
138
+ module.counts.active = (module.counts.active || 0) + 1;
139
+ }
140
+ if (stateValue !== "active") module.counts[stateValue] = (module.counts[stateValue] || 0) + 1;
141
+ if (uiDashboardTaskHasRisk(task)) {
142
+ module.counts.risk = (module.counts.risk || 0) + 1;
143
+ }
144
+ if (task.briefSource && task.briefSource !== "standalone") {
145
+ module.counts.missingDocs = (module.counts.missingDocs || 0) + 1;
8
146
  }
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"))}
147
+ }
148
+
149
+ function uiDashboardTaskHasRisk(task) {
150
+ if (task.state === "blocked") return true;
151
+ if (String(task.reviewStatus || "").includes("blocked")) return true;
152
+ if (Array.isArray(task.materialIssues) && task.materialIssues.length > 0) return true;
153
+ if (Array.isArray(task.queueReasons) && task.queueReasons.length > 0) return true;
154
+ if (String(task.visualMapStatus || "") === "missing") return true;
155
+ return false;
156
+ }
157
+
158
+ function moduleRunStrip(modules, unclassified) {
159
+ const active = modules.filter((module) => Number(module.counts?.active || 0) > 0).length;
160
+ const risk = modules.reduce((sum, module) => sum + Number(module.counts?.risk || 0), 0);
161
+ const registered = modules.filter((module) => module.source === "registry").length;
162
+ return `<section class="module-run-strip">
163
+ ${metric(t("moduleRegistered"), registered)}
164
+ ${metric(t("moduleActive"), active)}
165
+ ${metric(t("moduleRisks"), risk)}
166
+ ${metric(t("moduleUnclassified"), unclassified.length)}
167
+ </section>`;
168
+ }
169
+
170
+ function moduleListItem(module, active) {
171
+ const counts = module.counts || emptyUiModuleCounts();
172
+ return `<a class="module-list-item ${active ? "active" : ""}" href="#/modules/${encodeURIComponent(module.key)}" data-module-select="${escapeAttr(module.key)}">
173
+ <span>
174
+ <strong>${escapeHtml(module.key === "base" ? t("baseModule") : module.title || module.key)}</strong>
175
+ <small>${escapeHtml(module.key)} · ${escapeHtml(module.source || "registry")}</small>
176
+ </span>
177
+ <span class="module-list-counts">
178
+ <b>${Number(counts.active || 0)}</b>
179
+ ${tag(module.status || "planned")}
180
+ </span>
181
+ </a>`;
182
+ }
183
+
184
+ function moduleDetail(module) {
185
+ const tasks = normalCycleTasks().filter((task) => taskModuleKey(task) === module.key);
186
+ const activeTasks = tasks.filter((task) => ["in_progress", "review", "blocked", "planned", "not_started"].includes(task.state));
187
+ const riskTasks = tasks.filter((task) => task.state === "blocked" || String(task.reviewStatus || "").includes("blocked") || String(task.visualMapStatus || "") === "missing");
188
+ const brief = findDocument(module.briefPath || `TARGET:coding-agent-harness/planning/modules/${module.key}/brief.md`);
189
+ const plan = findDocument(module.modulePlanPath || "");
190
+ return `<div class="module-detail-stack">
191
+ <header class="module-detail-header">
192
+ <div>
193
+ <p class="eyebrow">${escapeHtml(module.key === "base" ? t("baseModuleEyebrow") : module.source === "registry" ? t("registeredModule") : t("inferredModule"))}</p>
194
+ <h2>${escapeHtml(module.key === "base" ? t("baseModule") : module.title || module.key)}</h2>
195
+ <p class="subtle">${escapeHtml(module.key)}${module.currentStep ? ` · ${escapeHtml(module.currentStep)}` : ""}</p>
196
+ </div>
197
+ ${tag(module.status || "planned")}
198
+ </header>
199
+ <div class="module-chip-row">
200
+ ${module.owner ? `<span class="module-chip">${t("moduleOwner")}: ${escapeHtml(module.owner)}</span>` : ""}
201
+ ${module.branch ? `<span class="module-chip">${t("moduleBranch")}: ${escapeHtml(module.branch)}</span>` : ""}
202
+ ${module.dependsOn?.length ? `<span class="module-chip">${t("moduleDependsOn")}: ${escapeHtml(module.dependsOn.join(", "))}</span>` : ""}
203
+ </div>
204
+ <section class="module-boundary-grid">
205
+ ${moduleBoundaryBlock(t("moduleScope"), module.scope)}
206
+ ${moduleBoundaryBlock(t("moduleShared"), module.shared)}
207
+ ${moduleBoundaryBlock(t("moduleDependsOn"), module.dependsOn)}
13
208
  </section>
14
- </main>`;
209
+ <section class="module-work-panel">
210
+ <div class="section-head">
211
+ <div>
212
+ <h3>${t("moduleCurrentWork")}</h3>
213
+ <p class="subtle">${activeTasks.length} ${t("active")} · ${riskTasks.length} ${t("moduleRisks")}</p>
214
+ </div>
215
+ <a href="#/tasks">${t("openTaskIndex")}</a>
216
+ </div>
217
+ <div class="module-task-list">
218
+ ${activeTasks.slice(0, 10).map(moduleTaskRow).join("") || `<p>${t("noModuleTasks")}</p>`}
219
+ </div>
220
+ </section>
221
+ ${riskTasks.length ? `<section class="module-risk-panel">
222
+ <h3>${t("moduleRiskPanel")}</h3>
223
+ <div class="module-task-list">${riskTasks.slice(0, 8).map(moduleTaskRow).join("")}</div>
224
+ </section>` : ""}
225
+ <section class="module-doc-panel">
226
+ <h3>${t("sourceDocuments")}</h3>
227
+ <div class="module-doc-links">
228
+ ${moduleDocLink(t("brief"), module.briefPath, brief)}
229
+ ${moduleDocLink(t("taskPlan"), module.modulePlanPath, plan)}
230
+ </div>
231
+ <div class="markdown module-doc-preview">${brief ? window.HarnessMarkdown.render(brief.content, "rendered") : `<p>${t("moduleBriefMissing")}</p>`}</div>
232
+ </section>
233
+ </div>`;
234
+ }
235
+
236
+ function moduleBoundaryBlock(title, values) {
237
+ const items = Array.isArray(values) && values.length ? values : [t("none")];
238
+ return `<div class="module-boundary-block">
239
+ <strong>${escapeHtml(title)}</strong>
240
+ ${items.map((item) => `<span>${escapeHtml(item)}</span>`).join("")}
241
+ </div>`;
242
+ }
243
+
244
+ function moduleDocLink(labelText, pathValue, document) {
245
+ if (!pathValue && !document) return `<span class="module-doc-link missing">${escapeHtml(labelText)} · ${t("documentMissing")}</span>`;
246
+ return `<span class="module-doc-link">${escapeHtml(labelText)} · ${escapeHtml(pathValue || document.path || t("ready"))}</span>`;
15
247
  }
16
248
 
17
249
  function moduleTaskRow(task) {
18
250
  const dotClass = /fail|blocked|open/i.test(task.state) ? "state-fail" : /warn|advice|planned|missing|unknown/i.test(task.state) ? "state-warn" : "state-pass";
251
+ const lifecycle = [task.lifecycleState, task.reviewStatus, task.closeoutStatus].filter(Boolean).map((item) => label(item)).join(" · ");
19
252
  return `<a class="module-task-row" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}">
20
253
  <div class="module-task-left">
21
254
  <i class="module-task-dot ${dotClass}" title="${escapeAttr(task.state)}"></i>
22
- <span class="module-task-title">${escapeHtml(task.title)}</span>
255
+ <span class="module-task-title">${escapeHtml(task.title || task.id)}</span>
256
+ ${lifecycle ? `<small>${escapeHtml(lifecycle)}</small>` : ""}
23
257
  </div>
24
- <span class="module-task-pct">${task.completion}%</span>
258
+ <span class="module-task-pct">${clampCompletion(task.completion)}%</span>
25
259
  </a>`;
26
260
  }
27
261
 
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(module.briefPath || `TARGET:coding-agent-harness/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>`}
262
+ function moduleUnclassifiedPanel(tasks) {
263
+ return `<section class="module-unclassified-panel">
264
+ <div class="section-head">
265
+ <div>
266
+ <p class="eyebrow">${t("unclassifiedWarning")}</p>
267
+ <h2>${t("unclassifiedModule")}</h2>
268
+ <p class="subtle">${t("unclassifiedSummary").replace("{count}", String(tasks.length))}</p>
269
+ </div>
270
+ <a href="#/tasks">${t("openTaskIndex")}</a>
55
271
  </div>
56
- ${pagerHtml}
57
- </article>`;
272
+ <div class="module-task-list">${tasks.slice(0, 12).map(moduleTaskRow).join("")}</div>
273
+ </section>`;
58
274
  }
@@ -6,6 +6,10 @@ function reviewQueue() {
6
6
  const reasonOptions = reviewReasonOptions(baseTasks);
7
7
  normalizeReviewReasonFilter(reasonOptions);
8
8
  const tasks = reviewFilteredTasks(baseTasks);
9
+ const confirmableTasks = activeTab.id === "review" ? tasks.filter(taskCanBeHumanConfirmed) : [];
10
+ syncReviewBulkSelection(confirmableTasks);
11
+ if (activeTab.id === "lessons") syncLessonBulkSelection(lessonBulkActionableSelections());
12
+ else syncLessonBulkSelection([]);
9
13
  const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
10
14
  const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
11
15
  const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
@@ -41,6 +45,8 @@ function reviewQueue() {
41
45
  </select>
42
46
  </div>
43
47
  </div>
48
+ ${activeTab.id === "review" ? reviewBulkBar(confirmableTasks) : ""}
49
+ ${activeTab.id === "lessons" ? lessonBulkBar() : ""}
44
50
  <div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
45
51
  <div class="review-queue-list">
46
52
  ${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
@@ -107,7 +113,7 @@ function reviewSortOptions() {
107
113
  }
108
114
 
109
115
  function reviewQueueBaseTasks(tab) {
110
- return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
116
+ return normalCycleTasks().filter((task) => taskMatchesReviewTab(task, tab));
111
117
  }
112
118
 
113
119
  function taskMatchesReviewTab(task, tab) {
@@ -183,17 +189,57 @@ function reviewTruthyCount(tasks, key) {
183
189
  return tasks.filter((task) => task[key] === true).length;
184
190
  }
185
191
 
192
+ function reviewBulkSelectedIds() {
193
+ return Object.entries(state.reviewBulkSelection || {})
194
+ .filter(([, selected]) => selected === true)
195
+ .map(([taskId]) => taskId);
196
+ }
197
+
198
+ function syncReviewBulkSelection(confirmableTasks) {
199
+ const allowed = new Set(confirmableTasks.map((task) => task.id));
200
+ for (const taskId of Object.keys(state.reviewBulkSelection || {})) {
201
+ if (!allowed.has(taskId)) delete state.reviewBulkSelection[taskId];
202
+ }
203
+ }
204
+
205
+ function reviewBulkBar(confirmableTasks) {
206
+ const selectedCount = reviewBulkSelectedIds().length;
207
+ const allSelected = confirmableTasks.length > 0 && confirmableTasks.every((task) => state.reviewBulkSelection?.[task.id] === true);
208
+ const disabled = selectedCount === 0 || !canUseWorkbenchAction("review-complete-bulk");
209
+ const result = state.reviewBulkResult ? `<span class="bulk-action-result ${state.reviewBulkResult.ok ? "success" : "failed"}">${escapeHtml(state.reviewBulkResult.message)}</span>` : "";
210
+ return `<div class="bulk-action-bar review-bulk-bar">
211
+ <label class="bulk-select-all">
212
+ <input type="checkbox" data-review-bulk-select-all ${allSelected ? "checked" : ""} ${confirmableTasks.length ? "" : "disabled"} aria-label="${escapeAttr(t("selectAllReviewTasks"))}">
213
+ <span>${t("selectAllReviewTasks")}</span>
214
+ </label>
215
+ <span class="bulk-selected-count">${formatMessage("reviewBulkSelected", { count: selectedCount })}</span>
216
+ <button type="button" data-review-bulk-confirm ${disabled ? "disabled" : ""}>${t("reviewBulkConfirm")}</button>
217
+ <button type="button" data-review-bulk-clear ${selectedCount ? "" : "disabled"}>${t("clearSelection")}</button>
218
+ ${result}
219
+ </div>`;
220
+ }
221
+
186
222
  function reviewQueueCard(task, tab) {
187
223
  const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
188
224
  const reasons = task.queueReasons || [];
189
225
  const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
190
226
  const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
227
+ const closeoutAction = taskReadyForCloseout(task)
228
+ ? `<button data-task-complete="${escapeAttr(task.id)}" ${canUseWorkbenchAction("task-complete") ? "" : "disabled"}>${t("completeTaskCloseout")}</button><span class="inline-result" data-task-complete-result="${escapeAttr(task.id)}"></span>`
229
+ : "";
191
230
  const displayId = task.shortId || taskFolderName(task) || task.id;
231
+ const canBulkConfirm = tab?.id === "review" && taskCanBeHumanConfirmed(task);
232
+ const bulkSelected = state.reviewBulkSelection?.[task.id] === true;
233
+ const bulkControl = tab?.id === "review" ? `<label class="bulk-card-check">
234
+ <input type="checkbox" data-review-bulk-select="${escapeAttr(task.id)}" ${canBulkConfirm ? "" : "disabled"} ${bulkSelected ? "checked" : ""} aria-label="${escapeAttr(t("selectReviewTask"))}">
235
+ <span>${t("select")}</span>
236
+ </label>` : "";
192
237
  return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
193
238
  <div class="card-header">
194
239
  <span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
195
240
  ${tag(task.reviewStatus || "missing")}
196
241
  ${reviewTaskQueues(task).map(tag).join("")}
242
+ ${bulkControl}
197
243
  </div>
198
244
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
199
245
  <div class="card-meta">
@@ -204,23 +250,39 @@ function reviewQueueCard(task, tab) {
204
250
  <span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
205
251
  </div>
206
252
  <p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
253
+ ${tombstoneSummary(task)}
207
254
  ${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
208
255
  ${lessonActions}
209
256
  <div class="review-queue-actions">
210
257
  <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
211
258
  <a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
212
259
  <button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
260
+ ${closeoutAction}
213
261
  ${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
214
262
  </div>
215
263
  </article>`;
216
264
  }
217
265
 
266
+ function tombstoneSummary(task) {
267
+ const deletionState = String(task?.deletionState || "active");
268
+ if (deletionState === "active") return "";
269
+ const reason = String(task?.deleteReason || "").trim();
270
+ const supersededBy = String(task?.supersededBy || "").trim();
271
+ return `<div class="review-tombstone-summary">
272
+ <span>${tag(deletionState)}</span>
273
+ ${reason ? `<span>${t("reason")}: ${escapeHtml(reason)}</span>` : ""}
274
+ ${supersededBy ? `<a href="#/tasks/${encodeURIComponent(supersededBy)}">${escapeHtml(supersededBy)}</a>` : ""}
275
+ </div>`;
276
+ }
277
+
218
278
  function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
219
279
  const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
220
280
  if (!candidates.length) return "";
221
281
  const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
222
282
  const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
223
283
  const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
284
+ syncLessonBulkSelection(lessonBulkActionableSelections());
285
+ const bulkBar = context === "card" ? "" : lessonBulkBar();
224
286
  return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
225
287
  <div class="lesson-candidate-panel-head">
226
288
  <div>
@@ -230,6 +292,7 @@ function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
230
292
  <span class="tag">${visibleCandidates.length}/${candidates.length}</span>
231
293
  </div>
232
294
  ${staticNote}
295
+ ${bulkBar}
233
296
  <div class="lesson-candidate-actions">
234
297
  ${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
235
298
  </div>
@@ -241,6 +304,9 @@ function lessonCandidateAction(task, candidate) {
241
304
  const followUp = String(candidate.followUpTask || "").trim();
242
305
  const hasFollowUp = followUp && !/^pending$/i.test(followUp);
243
306
  const prompt = lessonSedimentationPrompt(task, candidate);
307
+ const selectionKey = lessonBulkSelectionKey(task.id, candidate.id);
308
+ const canBulkCreate = canUseWorkbenchAction("lesson-sedimentation-bulk") && !hasFollowUp;
309
+ const selected = state.lessonBulkSelection?.[selectionKey] === true;
244
310
  return `<div class="lesson-candidate-action">
245
311
  <div class="lesson-candidate-main">
246
312
  <strong>${escapeHtml(candidate.id)}</strong>
@@ -249,6 +315,10 @@ function lessonCandidateAction(task, candidate) {
249
315
  </div>
250
316
  <span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
251
317
  <div class="lesson-candidate-command-row">
318
+ <label class="bulk-card-check lesson-bulk-check">
319
+ <input type="checkbox" data-lesson-bulk-select="${escapeAttr(selectionKey)}" ${canBulkCreate ? "" : "disabled"} ${selected ? "checked" : ""} aria-label="${escapeAttr(t("selectLessonCandidate"))}">
320
+ <span>${t("select")}</span>
321
+ </label>
252
322
  ${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
253
323
  <button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
254
324
  <button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
@@ -256,6 +326,62 @@ function lessonCandidateAction(task, candidate) {
256
326
  </div>`;
257
327
  }
258
328
 
329
+ function lessonBulkSelectionKey(taskId, candidateId) {
330
+ return `${taskId}::${candidateId}`;
331
+ }
332
+
333
+ function parseLessonBulkSelectionKey(key) {
334
+ const separator = String(key || "").lastIndexOf("::");
335
+ if (separator < 0) return null;
336
+ const taskId = key.slice(0, separator);
337
+ const candidateId = key.slice(separator + 2);
338
+ if (!taskId || !candidateId) return null;
339
+ return { taskId, candidateId };
340
+ }
341
+
342
+ function lessonBulkSelectedSelections() {
343
+ return Object.entries(state.lessonBulkSelection || {})
344
+ .filter(([, selected]) => selected === true)
345
+ .map(([key]) => parseLessonBulkSelectionKey(key))
346
+ .filter(Boolean);
347
+ }
348
+
349
+ function lessonBulkActionableSelections() {
350
+ return (bundle.status?.tasks || []).flatMap((task) => (task.lessonCandidateRows || [])
351
+ .filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status))
352
+ .filter((candidate) => {
353
+ const followUp = String(candidate.followUpTask || "").trim();
354
+ return !followUp || /^pending$/i.test(followUp);
355
+ })
356
+ .map((candidate) => ({ taskId: task.id, candidateId: candidate.id })));
357
+ }
358
+
359
+ function syncLessonBulkSelection(actionableSelections) {
360
+ const allowed = new Set(actionableSelections.map((selection) => lessonBulkSelectionKey(selection.taskId, selection.candidateId)));
361
+ for (const key of Object.keys(state.lessonBulkSelection || {})) {
362
+ if (!allowed.has(key)) delete state.lessonBulkSelection[key];
363
+ }
364
+ }
365
+
366
+ function lessonBulkBar() {
367
+ const actionableSelections = lessonBulkActionableSelections();
368
+ syncLessonBulkSelection(actionableSelections);
369
+ const selectedCount = lessonBulkSelectedSelections().length;
370
+ const allSelected = actionableSelections.length > 0 && actionableSelections.every((selection) => state.lessonBulkSelection?.[lessonBulkSelectionKey(selection.taskId, selection.candidateId)] === true);
371
+ const disabled = selectedCount === 0 || !canUseWorkbenchAction("lesson-sedimentation-bulk");
372
+ const result = state.lessonBulkResult ? `<span class="bulk-action-result ${state.lessonBulkResult.ok ? "success" : "failed"}">${escapeHtml(state.lessonBulkResult.message)}</span>` : "";
373
+ return `<div class="bulk-action-bar lesson-bulk-bar">
374
+ <label class="bulk-select-all">
375
+ <input type="checkbox" data-lesson-bulk-select-all ${allSelected ? "checked" : ""} ${actionableSelections.length ? "" : "disabled"} aria-label="${escapeAttr(t("selectAllLessonCandidates"))}">
376
+ <span>${t("selectAllLessonCandidates")}</span>
377
+ </label>
378
+ <span class="bulk-selected-count">${formatMessage("lessonBulkSelected", { count: selectedCount })}</span>
379
+ <button type="button" data-lesson-bulk-create ${disabled ? "disabled" : ""}>${t("lessonBulkCreate")}</button>
380
+ <button type="button" data-lesson-bulk-clear ${selectedCount ? "" : "disabled"}>${t("clearSelection")}</button>
381
+ ${result}
382
+ </div>`;
383
+ }
384
+
259
385
  function lessonSedimentationPrompt(task, candidate) {
260
386
  return [
261
387
  "You are executing a lesson sedimentation follow-up task.",