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
@@ -12,9 +12,22 @@ const state = {
12
12
  taskGroupPage: 1,
13
13
  warningFilter: "all",
14
14
  warningPage: 1,
15
+ presetQuery: "",
16
+ presetSourceFilter: "all",
17
+ selectedPresetKey: "",
18
+ selectedPresetId: "",
19
+ presetActionResult: null,
20
+ presetInstallSource: "",
21
+ presetInstallScope: "project",
22
+ presetInstallForce: false,
23
+ presetSeedScope: "project",
24
+ presetSeedForce: false,
25
+ presetUninstallScope: "project",
26
+ presetUninstallConfirm: "",
15
27
  renderMode: "rendered",
16
28
  theme: localStorage.getItem("harness.theme") || "system",
17
29
  taskLayout: localStorage.getItem("harness.taskLayout") || "list",
30
+ taskSortOrder: localStorage.getItem("harness.taskSortOrder") === "asc" ? "asc" : "desc",
18
31
  runtime: { mode: "static", csrfToken: "", writableActions: [] },
19
32
  runtimeLoaded: false,
20
33
  runtimePoller: null,
@@ -30,6 +43,8 @@ const taskDocTabs = [
30
43
  ["strategy", "execution_strategy.md"],
31
44
  ["visualMap", "visual_map.md"],
32
45
  ["legacyRoadmap", "visual_roadmap.md"],
46
+ ["lessonCandidates", "lesson_candidates.md"],
47
+ ["longRunningContract", "long-running-task-contract.md"],
33
48
  ["progress", "progress.md"],
34
49
  ["review", "review.md"],
35
50
  ["findings", "findings.md"],
@@ -69,21 +84,34 @@ function shell() {
69
84
  ${routeLink("#/tasks", t("taskIndex"), "tasks")}
70
85
  ${routeLink("#/review", t("reviewQueue"), "review")}
71
86
  ${routeLink("#/modules", t("moduleView"), "modules")}
87
+ ${routeLink("#/presets", t("presetCatalog"), "presets")}
72
88
  <button data-language-toggle>${locale === "zh" ? "EN" : "中文"}</button>
73
89
  <button data-theme-toggle>${themeLabel()}</button>
74
90
  </div>
75
91
  </header>
92
+ ${runtimeModeBanner()}
76
93
  ${renderRoute()}
77
94
  <div id="drawer-overlay" class="drawer-overlay"></div>
78
95
  <div id="task-drawer" class="task-drawer"></div>
79
96
  </div>`;
80
97
  }
81
98
 
99
+ function runtimeModeBanner() {
100
+ if (window.__HARNESS_WORKBENCH__ === true) return "";
101
+ return `<section class="runtime-banner">
102
+ <strong>${t("staticReadOnly")}</strong>
103
+ <span>${t("staticReadOnlyDetail")}</span>
104
+ <code>harness dev</code>
105
+ </section>`;
106
+ }
107
+
82
108
  function renderRoute() {
83
109
  const route = currentRoute();
84
110
  if (route.name === "task") return taskDetail(route);
111
+ if (route.name === "reviewTask") return reviewWorkspace(route);
85
112
  if (route.name === "review") return reviewQueue();
86
113
  if (route.name === "modules") return modulesView(route.id);
114
+ if (route.name === "presets") return presetsView();
87
115
  if (route.name === "tasks") return taskIndex();
88
116
  return overview();
89
117
  }
@@ -92,14 +120,17 @@ function currentRoute() {
92
120
  const hash = window.location.hash || "#/";
93
121
  const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean).map(decodeURIComponent);
94
122
  if (parts[0] === "tasks" && parts[1]) return { name: "task", id: parts[1], doc: parts[2] === "docs" ? parts[3] || "" : "" };
123
+ if (parts[0] === "review" && parts[1]) return { name: "reviewTask", id: parts[1] };
95
124
  if (parts[0] === "review") return { name: "review" };
96
125
  if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
126
+ if (parts[0] === "presets") return { name: "presets" };
97
127
  if (parts[0] === "tasks") return { name: "tasks" };
98
128
  return { name: "overview" };
99
129
  }
100
130
 
101
131
  function routeLink(hash, text, routeName) {
102
- const active = currentRoute().name === routeName;
132
+ const current = currentRoute().name;
133
+ const active = current === routeName || (routeName === "review" && current === "reviewTask");
103
134
  return `<a class="${active ? "active" : ""}" href="${hash}">${escapeHtml(text)}</a>`;
104
135
  }
105
136
 
@@ -121,6 +152,9 @@ function overview() {
121
152
 
122
153
  function statusStrip() {
123
154
  const status = bundle.status?.checkState?.status || "unknown";
155
+ const validationMode = bundle.status?.checkState?.validationMode || "validated";
156
+ const dataOnly = validationMode === "data-only";
157
+ const displayState = dataOnly ? "snapshot" : status;
124
158
  const failures = bundle.status?.checkState?.failures || 0;
125
159
  const warnings = bundle.status?.checkState?.warnings || 0;
126
160
  const tasks = bundle.status?.tasks || [];
@@ -128,9 +162,9 @@ function statusStrip() {
128
162
  const visual = summary.visualMapCoverage || {};
129
163
  const withBrief = tasks.filter((task) => task.briefSource === "standalone").length;
130
164
  return `<section class="status-card-group">
131
- <div class="status-primary ${status}">
132
- <span>${t("readiness")}</span>
133
- <strong>${label(status)}</strong>
165
+ <div class="status-primary ${displayState}">
166
+ <span>${dataOnly ? t("snapshotStatus") : t("readiness")}</span>
167
+ <strong>${dataOnly ? t("snapshot") : label(status)}</strong>
134
168
  <p>${nextActionText()}</p>
135
169
  </div>
136
170
  <div class="metrics-grid">
@@ -151,6 +185,7 @@ function metric(labelText, value) {
151
185
  }
152
186
 
153
187
  function nextActionText() {
188
+ if ((bundle.status?.checkState?.validationMode || "validated") === "data-only") return t("snapshotNotValidated");
154
189
  const failures = bundle.status?.checkState?.failures || 0;
155
190
  if (failures > 0) return t("resolveBlockers");
156
191
  const missingBriefs = (bundle.status?.tasks || []).filter((task) => task.briefSource !== "standalone").length;
@@ -270,24 +305,29 @@ function graphSummary() {
270
305
  }
271
306
 
272
307
  function activeTaskBriefs() {
273
- const tasks = activeTasks().slice(0, 8);
308
+ const tasks = activeTasks();
274
309
  return `<section class="task-briefs">
275
310
  <div class="section-head">
276
311
  <div>
277
312
  <p class="eyebrow">${t("currentWork")}</p>
278
313
  <h2>${t("activeBriefs")}</h2>
279
314
  </div>
280
- <a href="#/tasks">${t("openTaskIndex")}</a>
315
+ <div class="section-actions">
316
+ <span class="subtle">${t("activeBriefCount").replace("{count}", tasks.length).replace("{order}", taskSortLabel())}</span>
317
+ <a href="#/tasks">${t("openTaskIndex")}</a>
318
+ </div>
319
+ </div>
320
+ <div class="brief-scroll">
321
+ <div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
281
322
  </div>
282
- <div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
283
323
  </section>`;
284
324
  }
285
325
 
286
326
  function activeTasks() {
287
327
  const tasks = bundle.status?.tasks || [];
288
328
  const active = tasks.filter((task) => isActiveTaskState(task.state) || ["planned", "not_started"].includes(task.state));
289
- if (active.length > 0) return active;
290
- return tasks.filter((task) => task.briefSource === "standalone").slice(0, 6);
329
+ if (active.length > 0) return sortTasksByTime(active);
330
+ return sortTasksByTime(tasks.filter((task) => task.briefSource === "standalone"));
291
331
  }
292
332
 
293
333
  function isActiveTaskState(state) {
@@ -310,6 +350,7 @@ function taskBriefCard(task, { compact = true } = {}) {
310
350
  <p class="brief-teaser">${escapeHtml(summaryText)}</p>
311
351
  </div>
312
352
  <div class="card-actions">
353
+ ${taskCopyButton(task)}
313
354
  <button class="btn-drawer-trigger" data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
314
355
  </div>
315
356
  </article>`;
@@ -349,6 +390,59 @@ function stateToColorVar(state) {
349
390
  return map[state] || "--muted";
350
391
  }
351
392
 
393
+ function taskSortLabel() {
394
+ return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
395
+ }
396
+
397
+ function taskDateKey(task) {
398
+ const source = `${task.shortId || ""} ${task.id || ""}`.trim();
399
+ const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
400
+ if (!match) return null;
401
+ const year = Number(match[1]);
402
+ const month = Number(match[2]);
403
+ const day = Number(match[3] || "1");
404
+ if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
405
+ return Date.UTC(year, month - 1, day);
406
+ }
407
+
408
+ function stableTaskLabel(task) {
409
+ return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
410
+ }
411
+
412
+ function compareTasksByTime(left, right) {
413
+ const leftDate = taskDateKey(left);
414
+ const rightDate = taskDateKey(right);
415
+ if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
416
+ return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
417
+ }
418
+ if (leftDate !== null && rightDate === null) return -1;
419
+ if (leftDate === null && rightDate !== null) return 1;
420
+ return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
421
+ }
422
+
423
+ function sortTasksByTime(tasks) {
424
+ return [...tasks].sort(compareTasksByTime);
425
+ }
426
+
427
+ function taskFolderName(task) {
428
+ const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
429
+ const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
430
+ return task?.shortId || fromPath || fromId || task?.title || "";
431
+ }
432
+
433
+ function taskCopyButton(task, extraClass = "") {
434
+ const folderName = taskFolderName(task);
435
+ return `<button type="button" class="copy-task-name ${extraClass}" data-copy-task-name="${escapeAttr(folderName)}" data-copy-task-folder="${escapeAttr(folderName)}" aria-label="${escapeAttr(t("copyTaskName"))}" title="${escapeAttr(t("copyTaskName"))}">
436
+ ${t("copyTaskNameShort")}
437
+ </button>`;
438
+ }
439
+
440
+ function taskGroupTimeKey(group) {
441
+ const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
442
+ if (!match) return null;
443
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
444
+ }
445
+
352
446
  function taskToolbarCard(filteredCount) {
353
447
  return `<section class="sidebar-card">
354
448
  <h3>${t("filterTitle")}</h3>
@@ -380,6 +474,17 @@ function taskToolbarCard(filteredCount) {
380
474
  </button>
381
475
  </div>
382
476
  </div>
477
+ <div class="select-group">
478
+ <label>${t("sortByTime")}</label>
479
+ <div class="layout-toggle-group sort-toggle-group">
480
+ <button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
481
+ ${t("sortNewest")}
482
+ </button>
483
+ <button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
484
+ ${t("sortOldest")}
485
+ </button>
486
+ </div>
487
+ </div>
383
488
  <div class="search-stats">
384
489
  ${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
385
490
  </div>
@@ -483,11 +588,12 @@ function taskRow(task) {
483
588
  const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
484
589
  const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
485
590
 
486
- return `<a class="task-row-card" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
591
+ return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
487
592
  <div class="row-accent-bar"></div>
488
593
  <div class="row-main">
489
594
  <strong>${escapeHtml(task.title)}</strong>
490
595
  <span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
596
+ ${taskCopyButton(task, "row-copy")}
491
597
  </div>
492
598
  <div class="row-status">${tag(task.state)}</div>
493
599
  <div class="row-progress">
@@ -506,7 +612,7 @@ function taskRow(task) {
506
612
  ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
507
613
  </span>
508
614
  </div>
509
- </a>`;
615
+ </article>`;
510
616
  }
511
617
 
512
618
  function taskIndex() {
@@ -545,7 +651,18 @@ function orderedTaskGroups(groups) {
545
651
  if (group === "unknown") return 3;
546
652
  return 4;
547
653
  };
548
- return Object.entries(groups).sort(([left], [right]) => rank(left) - rank(right) || left.localeCompare(right));
654
+ return Object.entries(groups).sort(([left], [right]) => {
655
+ const rankDiff = rank(left) - rank(right);
656
+ if (rankDiff !== 0) return rankDiff;
657
+ const leftTime = taskGroupTimeKey(left);
658
+ const rightTime = taskGroupTimeKey(right);
659
+ if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
660
+ return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
661
+ }
662
+ if (leftTime !== null && rightTime === null) return -1;
663
+ if (leftTime === null && rightTime !== null) return 1;
664
+ return left.localeCompare(right);
665
+ });
549
666
  }
550
667
 
551
668
  function taskGroups(tasks) {
@@ -570,11 +687,12 @@ function taskGroups(tasks) {
570
687
  }
571
688
 
572
689
  function taskGroup(group, tasks) {
573
- const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
690
+ const orderedTasks = sortTasksByTime(tasks);
691
+ const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
574
692
  const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
575
693
  const start = (page - 1) * taskPageSize;
576
- const visibleTasks = tasks.slice(start, start + taskPageSize);
577
- const avgCompletion = tasks.length ? clampCompletion(tasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / tasks.length) : 0;
694
+ const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
695
+ const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
578
696
 
579
697
  const isGrid = state.taskLayout === "grid";
580
698
  const layoutClass = isGrid ? "task-card-grid" : "task-list";
@@ -591,7 +709,7 @@ function taskGroup(group, tasks) {
591
709
  <div class="section-head">
592
710
  <div>
593
711
  <h2>${taskGroupLabel(group)}</h2>
594
- <p class="subtle">${t("showing")} ${Math.min(start + 1, tasks.length)}-${Math.min(start + visibleTasks.length, tasks.length)} / ${tasks.length}</p>
712
+ <p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
595
713
  </div>
596
714
  <div class="group-actions">
597
715
  <div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
@@ -616,10 +734,13 @@ function taskCard(task) {
616
734
  const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
617
735
  const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
618
736
 
619
- return `<a class="task-card" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
737
+ return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
620
738
  <div class="card-header">
621
739
  <span class="card-id">${escapeHtml(task.id)}</span>
622
- ${tag(task.state)}
740
+ <div class="card-header-actions">
741
+ ${taskCopyButton(task, "compact")}
742
+ ${tag(task.state)}
743
+ </div>
623
744
  </div>
624
745
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
625
746
  <div class="card-meta">
@@ -642,7 +763,7 @@ function taskCard(task) {
642
763
  ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
643
764
  </span>
644
765
  </div>
645
- </a>`;
766
+ </article>`;
646
767
  }
647
768
 
648
769
  function taskGroupLabel(group) {
@@ -657,12 +778,12 @@ function taskGroupLabel(group) {
657
778
 
658
779
  function filteredTasks() {
659
780
  const query = state.query.trim().toLowerCase();
660
- return (bundle.status?.tasks || []).filter((task) => {
781
+ return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
661
782
  const stateMatch = state.taskState === "all" || task.state === state.taskState;
662
783
  if (!stateMatch) return false;
663
784
  if (!query) return true;
664
785
  return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
665
- });
786
+ }));
666
787
  }
667
788
 
668
789
  function taskModuleKey(task) {
@@ -680,6 +801,7 @@ function taskDetail(route) {
680
801
  <p class="eyebrow">${t("taskVisibility")}</p>
681
802
  <h2>${escapeHtml(task.title)}</h2>
682
803
  <p>${escapeHtml(task.path)}</p>
804
+ ${taskCopyButton(task, "detail-copy")}
683
805
  </div>
684
806
  <div class="detail-score">${task.completion}%</div>
685
807
  </section>
@@ -690,7 +812,8 @@ function taskDetail(route) {
690
812
  ${taskDocumentLibrary(task, route.doc)}
691
813
  </article>
692
814
  <aside class="detail-side">
693
- ${reviewActionPanel(task)}
815
+ ${reviewActionPanel(task, { mode: "summary" })}
816
+ ${lessonCandidatePanel(task, { context: "detail" })}
694
817
  ${openFindings(task)}
695
818
  ${evidenceList(task)}
696
819
  ${documentTabs(task)}
@@ -713,25 +836,85 @@ function taskStateSummary(task) {
713
836
  <span>${t("reviewStatus")}</span>
714
837
  ${tag(task.reviewStatus || "missing")}
715
838
  </div>
839
+ <div>
840
+ <span>${t("sedimentationStatus")}</span>
841
+ ${tag(task.lessonCandidateStatus || "missing")}
842
+ </div>
716
843
  <div>
717
844
  <span>${t("closeoutStatus")}</span>
718
845
  ${tag(task.closeoutStatus || "missing")}
719
846
  </div>
847
+ <div>
848
+ <span>${t("lifecycleQueues")}</span>
849
+ ${(task.taskQueues || []).map(tag).join("") || tag("active")}
850
+ </div>
851
+ ${taskQueueReasonSummary(task)}
720
852
  </section>`;
721
853
  }
722
854
 
855
+ function taskQueueReasonSummary(task) {
856
+ const reasons = task.queueReasons || [];
857
+ if (!reasons.length) return "";
858
+ return `<div class="task-queue-reasons">
859
+ <span>${t("queueReasons")}</span>
860
+ <div class="review-reasons">
861
+ ${reasons.slice(0, 5).map(reviewReason).join("")}
862
+ </div>
863
+ </div>`;
864
+ }
865
+
723
866
  function phaseTimeline(task) {
867
+ const knownKinds = new Set(["init", "execution", "gate"]);
868
+ const groups = [
869
+ ["init", "Init"],
870
+ ["execution", "Execution"],
871
+ ["gate", "Gate"],
872
+ ["other", "Other / Invalid"],
873
+ ];
874
+ const phases = task.phases || [];
875
+ const grouped = groups
876
+ .map(([kind, label]) => {
877
+ const items = kind === "other"
878
+ ? phases.filter((phase) => !knownKinds.has(phase.kind || "execution"))
879
+ : phases.filter((phase) => (phase.kind || "execution") === kind);
880
+ if (!items.length) return "";
881
+ return `<div class="phase-kind-group ${escapeAttr(kind)}">
882
+ <h3>${escapeHtml(label)}</h3>
883
+ ${items.map(phaseStep).join("")}
884
+ </div>`;
885
+ })
886
+ .join("");
724
887
  return `<section class="phase-timeline">
725
888
  <h2>${t("phaseTimeline")}</h2>
726
- ${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
727
- <strong>${escapeHtml(phase.id)}</strong>
728
- <span>${phase.completion}%</span>
729
- <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
730
- ${progressBar(phase.completion)}
731
- </div>`).join("") || emptyState(t("noPhaseData"))}
889
+ ${grouped || emptyState(t("noPhaseData"))}
732
890
  </section>`;
733
891
  }
734
892
 
893
+ function phaseStep(phase) {
894
+ const kind = phase.kind || "execution";
895
+ const actor = phase.actor || "agent";
896
+ const knownKind = ["init", "execution", "gate"].includes(kind);
897
+ const kindLabel = knownKind ? escapeHtml(kind) : `<span class="tag warn">${escapeHtml(kind)}</span>`;
898
+ const phaseKindClass = knownKind ? kind : "other";
899
+ return `<div class="phase-step ${escapeAttr(phase.state)} ${escapeAttr(phaseKindClass)}">
900
+ <div class="phase-step-head">
901
+ <strong>${escapeHtml(phase.id)}</strong>
902
+ <span>${kindLabel} · ${phase.completion}%</span>
903
+ </div>
904
+ <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
905
+ ${progressBar(phase.completion)}
906
+ <div class="phase-meta">
907
+ ${phaseMetaTag(actor)}
908
+ ${tag(phase.evidenceStatus || "missing")}
909
+ </div>
910
+ ${phase.exitCommand ? `<code class="phase-exit-command">${escapeHtml(phase.exitCommand)}</code>` : ""}
911
+ </div>`;
912
+ }
913
+
914
+ function phaseMetaTag(value) {
915
+ return `<span class="tag">${escapeHtml(String(value || "unknown").replaceAll("_", " "))}</span>`;
916
+ }
917
+
735
918
  function taskDocSection(task, fileName, title, required) {
736
919
  const doc = taskDocument(task, fileName);
737
920
  if (!doc && !required) return "";
@@ -777,7 +960,7 @@ function taskDocumentPriority(task) {
777
960
  const stateName = task?.state || "";
778
961
  const lifecycle = task?.lifecycleState || "";
779
962
  if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
780
- return ["walkthrough", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "legacyRoadmap", "references", "artifacts"];
963
+ return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
781
964
  }
782
965
  if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
783
966
  return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
@@ -831,11 +1014,26 @@ function openFindings(task) {
831
1014
  </section>`;
832
1015
  }
833
1016
 
834
- function reviewActionPanel(task) {
835
- if (!canUseWorkbenchAction("review-complete")) return "";
836
- if (!isTaskInReviewStage(task)) return "";
1017
+ function reviewActionPanel(task, { mode = "summary" } = {}) {
1018
+ if (!isTaskInReviewQueue(task)) return "";
837
1019
  const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
838
1020
  const confirmed = task.reviewStatus === "confirmed";
1021
+ const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
1022
+ const candidateStatus = task.lessonCandidateStatus || "missing";
1023
+ if (mode !== "workspace") {
1024
+ return `<section class="side-panel review-actions">
1025
+ <h3>${t("reviewActions")}</h3>
1026
+ <p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
1027
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
1028
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
1029
+ </section>`;
1030
+ }
1031
+ if (!canUseWorkbenchAction("review-complete")) {
1032
+ return `<section class="side-panel review-actions">
1033
+ <h3>${t("reviewActions")}</h3>
1034
+ <p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
1035
+ </section>`;
1036
+ }
839
1037
  if (confirmed) {
840
1038
  return `<section class="side-panel review-actions">
841
1039
  <h3>${t("reviewActions")}</h3>
@@ -843,26 +1041,32 @@ function reviewActionPanel(task) {
843
1041
  </section>`;
844
1042
  }
845
1043
  const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
846
- const disabled = blocking || missingWalkthrough;
847
- const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : t("reviewWorkbenchReady");
1044
+ const queueBlocked = !taskCanBeHumanConfirmed(task);
1045
+ const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
1046
+ const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
848
1047
  return `<section class="side-panel review-actions">
849
1048
  <h3>${t("reviewActions")}</h3>
850
1049
  <p>${escapeHtml(message)}</p>
1050
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
851
1051
  <label class="review-check">
852
1052
  <input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
853
1053
  <span>${t("reviewConfirmChecklist")}</span>
854
1054
  </label>
1055
+ <div class="review-confirm-copy">
1056
+ ${taskCopyButton(task, "review-copy-task-name")}
1057
+ </div>
855
1058
  <input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
856
1059
  <button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
857
1060
  <div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
858
1061
  </section>`;
859
1062
  }
860
1063
 
861
- function isTaskInReviewStage(task) {
862
- const state = task?.state || "";
863
- const lifecycle = task?.lifecycleState || "";
864
- if (["not_started", "planned", "in_progress"].includes(state)) return false;
865
- return state === "review" || ["in_review", "review-blocked"].includes(lifecycle);
1064
+ function isTaskInReviewQueue(task) {
1065
+ return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
1066
+ }
1067
+
1068
+ function taskCanBeHumanConfirmed(task) {
1069
+ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
866
1070
  }
867
1071
 
868
1072
  function evidenceList(task) {
@@ -933,10 +1137,16 @@ function moduleCard(module) {
933
1137
  }
934
1138
 
935
1139
  function reviewQueue() {
936
- const tasks = reviewQueueTasks();
937
- const ready = tasks.filter((task) => task.reviewStatus !== "blocked-open-findings" && task.reviewStatus !== "confirmed").length;
938
- const blocked = tasks.filter((task) => task.reviewStatus === "blocked-open-findings").length;
939
- const confirmed = tasks.filter((task) => task.reviewStatus === "confirmed").length;
1140
+ ensureReviewQueueState();
1141
+ const tabs = reviewQueueTabs();
1142
+ const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
1143
+ const baseTasks = reviewQueueBaseTasks(activeTab);
1144
+ const reasonOptions = reviewReasonOptions(baseTasks);
1145
+ normalizeReviewReasonFilter(reasonOptions);
1146
+ const tasks = reviewFilteredTasks(baseTasks);
1147
+ const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
1148
+ const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
1149
+ const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
940
1150
  return `<div class="dashboard-grid review-queue-page">
941
1151
  <main class="dashboard-main stack">
942
1152
  <section class="flow-panel">
@@ -946,10 +1156,36 @@ function reviewQueue() {
946
1156
  <h2>${t("reviewQueue")}</h2>
947
1157
  <p class="subtle">${t("reviewQueueSubtitle")}</p>
948
1158
  </div>
949
- <span class="subtle">${ready}/${tasks.length} ${t("reviewReady")}</span>
1159
+ <span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
1160
+ </div>
1161
+ <div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
1162
+ ${tabs.map((tab) => reviewQueueTab(tab)).join("")}
1163
+ </div>
1164
+ <div class="review-queue-toolbar">
1165
+ <div class="input-group">
1166
+ <input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
1167
+ </div>
1168
+ <div class="select-group">
1169
+ <label>${t("reasonFilter")}</label>
1170
+ <select data-review-reason-filter aria-label="${t("reasonFilter")}">
1171
+ <option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
1172
+ ${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
1173
+ </select>
1174
+ </div>
1175
+ <div class="select-group">
1176
+ <label>${t("sortBy")}</label>
1177
+ <select data-review-sort aria-label="${t("sortBy")}">
1178
+ ${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
1179
+ </select>
1180
+ </div>
950
1181
  </div>
951
- <div class="task-card-grid review-queue-grid">
952
- ${tasks.map(reviewQueueCard).join("") || emptyState(t("noReviewTasks"))}
1182
+ <div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
1183
+ <div class="review-queue-list">
1184
+ ${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
1185
+ </div>
1186
+ </div>
1187
+ <div class="review-queue-pager">
1188
+ ${pager("review", page, pageCount)}
953
1189
  </div>
954
1190
  </section>
955
1191
  </main>
@@ -957,52 +1193,239 @@ function reviewQueue() {
957
1193
  <section class="side-panel review-queue-summary">
958
1194
  <h3>${t("reviewQueue")}</h3>
959
1195
  <div class="review-queue-stats">
960
- ${metric(t("reviewReady"), ready)}
961
- ${metric(t("reviewBlockedQueue"), blocked)}
962
- ${metric(t("reviewConfirmedQueue"), confirmed)}
1196
+ ${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
963
1197
  </div>
964
1198
  </section>
965
1199
  <section class="side-panel">
966
- <h3>${t("review")}</h3>
967
- <p>${escapeHtml(t("reviewQueueSubtitle"))}</p>
1200
+ <h3>${escapeHtml(activeTab.label)}</h3>
1201
+ <p>${escapeHtml(activeTab.description)}</p>
1202
+ <dl class="review-queue-contract">
1203
+ <div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
1204
+ <div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
1205
+ </dl>
968
1206
  </section>
969
1207
  </aside>
970
1208
  </div>`;
971
1209
  }
972
1210
 
973
- function reviewQueueTasks() {
974
- return (bundle.status?.tasks || [])
975
- .filter(isTaskInReviewStage)
976
- .sort((left, right) => reviewSortKey(left).localeCompare(reviewSortKey(right)));
1211
+ function ensureReviewQueueState() {
1212
+ if (!state.reviewQueueTab) state.reviewQueueTab = "review";
1213
+ if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
1214
+ if (!state.reviewSort) state.reviewSort = "queue";
1215
+ if (!state.reviewQueuePage) state.reviewQueuePage = 1;
1216
+ }
1217
+
1218
+ function reviewQueueTabs() {
1219
+ return [
1220
+ { id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
1221
+ { id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
1222
+ { id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
1223
+ { id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
1224
+ { id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
1225
+ { id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
1226
+ ];
977
1227
  }
978
1228
 
979
- function reviewSortKey(task) {
980
- const rank = task.reviewStatus === "blocked-open-findings" ? "0" : task.reviewStatus === "confirmed" ? "2" : "1";
981
- return `${rank}:${task.id}`;
1229
+ function reviewQueueTab(tab) {
1230
+ const active = tab.id === state.reviewQueueTab;
1231
+ const count = reviewQueueBaseTasks(tab).length;
1232
+ return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
1233
+ <span>${escapeHtml(tab.label)}</span>
1234
+ <strong>${count}</strong>
1235
+ </button>`;
982
1236
  }
983
1237
 
984
- function reviewQueueCard(task) {
1238
+ function reviewSortOptions() {
1239
+ return [
1240
+ { id: "queue", label: t("sortQueuePriority") },
1241
+ { id: "newest", label: t("sortNewest") },
1242
+ { id: "oldest", label: t("sortOldest") },
1243
+ { id: "id", label: t("sortTaskId") },
1244
+ ];
1245
+ }
1246
+
1247
+ function reviewQueueBaseTasks(tab) {
1248
+ return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
1249
+ }
1250
+
1251
+ function taskMatchesReviewTab(task, tab) {
1252
+ const queues = reviewTaskQueues(task);
1253
+ return (tab.queues || []).some((queue) => queues.includes(queue));
1254
+ }
1255
+
1256
+ function reviewTaskQueues(task) {
1257
+ return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
1258
+ }
1259
+
1260
+ function reviewReasonOptions(tasks) {
1261
+ return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
1262
+ }
1263
+
1264
+ function normalizeReviewReasonFilter(reasonOptions) {
1265
+ const current = state.reviewReasonFilter || "all";
1266
+ if (current === "all") return;
1267
+ if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
1268
+ }
1269
+
1270
+ function reviewFilteredTasks(tasks) {
1271
+ const query = state.query.trim().toLowerCase();
1272
+ const reasonFilter = state.reviewReasonFilter || "all";
1273
+ return [...tasks]
1274
+ .filter((task) => {
1275
+ if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
1276
+ if (!query) return true;
1277
+ return [
1278
+ task.id,
1279
+ task.shortId,
1280
+ task.title,
1281
+ task.module,
1282
+ task.inferredModule,
1283
+ task.state,
1284
+ task.lifecycleState,
1285
+ task.reviewStatus,
1286
+ task.closeoutStatus,
1287
+ ...(task.taskQueues || []),
1288
+ ...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
1289
+ ].some((value) => String(value || "").toLowerCase().includes(query));
1290
+ })
1291
+ .sort(reviewTaskSort);
1292
+ }
1293
+
1294
+ function reviewTaskSort(left, right) {
1295
+ if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
1296
+ if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
1297
+ if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
1298
+ return reviewPriorityRank(left) - reviewPriorityRank(right)
1299
+ || compareTasksByTimeForOrder(left, right, "desc")
1300
+ || stableTaskLabel(left).localeCompare(stableTaskLabel(right));
1301
+ }
1302
+
1303
+ function compareTasksByTimeForOrder(left, right, order) {
1304
+ const previous = state.taskSortOrder;
1305
+ state.taskSortOrder = order;
1306
+ const result = compareTasksByTime(left, right);
1307
+ state.taskSortOrder = previous;
1308
+ return result;
1309
+ }
1310
+
1311
+ function reviewPriorityRank(task) {
1312
+ const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
1313
+ const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
1314
+ const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
1315
+ const queues = reviewTaskQueues(task);
1316
+ const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
1317
+ return Math.min(reasonRank, taskQueueRank);
1318
+ }
1319
+
1320
+ function reviewTruthyCount(tasks, key) {
1321
+ return tasks.filter((task) => task[key] === true).length;
1322
+ }
1323
+
1324
+ function reviewQueueCard(task, tab) {
985
1325
  const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
1326
+ const reasons = task.queueReasons || [];
1327
+ const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
1328
+ const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
1329
+ const displayId = task.shortId || taskFolderName(task) || task.id;
986
1330
  return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
987
1331
  <div class="card-header">
988
- <span class="card-id">${escapeHtml(task.id)}</span>
1332
+ <span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
989
1333
  ${tag(task.reviewStatus || "missing")}
1334
+ ${reviewTaskQueues(task).map(tag).join("")}
990
1335
  </div>
991
1336
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
992
1337
  <div class="card-meta">
993
1338
  <span>${tag(task.lifecycleState || "unknown")}</span>
994
1339
  <span>${tag(task.closeoutStatus || "missing")}</span>
995
1340
  <span>${openMaterial} ${t("openFindings")}</span>
1341
+ <span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
1342
+ <span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
996
1343
  </div>
997
1344
  <p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
1345
+ ${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
1346
+ ${lessonActions}
998
1347
  <div class="review-queue-actions">
1348
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
999
1349
  <a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
1000
1350
  <button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
1351
+ ${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
1001
1352
  </div>
1002
- ${reviewActionPanel(task)}
1003
1353
  </article>`;
1004
1354
  }
1005
1355
 
1356
+ function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
1357
+ const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
1358
+ if (!candidates.length) return "";
1359
+ const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
1360
+ const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
1361
+ const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
1362
+ return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
1363
+ <div class="lesson-candidate-panel-head">
1364
+ <div>
1365
+ <p class="eyebrow">${t("lessonCandidates")}</p>
1366
+ <h3>${t("lessonSedimentationActions")}</h3>
1367
+ </div>
1368
+ <span class="tag">${visibleCandidates.length}/${candidates.length}</span>
1369
+ </div>
1370
+ ${staticNote}
1371
+ <div class="lesson-candidate-actions">
1372
+ ${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
1373
+ </div>
1374
+ ${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
1375
+ </section>`;
1376
+ }
1377
+
1378
+ function lessonCandidateAction(task, candidate) {
1379
+ const followUp = String(candidate.followUpTask || "").trim();
1380
+ const hasFollowUp = followUp && !/^pending$/i.test(followUp);
1381
+ const prompt = lessonSedimentationPrompt(task, candidate);
1382
+ return `<div class="lesson-candidate-action">
1383
+ <div class="lesson-candidate-main">
1384
+ <strong>${escapeHtml(candidate.id)}</strong>
1385
+ <span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
1386
+ <small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
1387
+ </div>
1388
+ <span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
1389
+ <div class="lesson-candidate-command-row">
1390
+ ${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
1391
+ <button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
1392
+ <button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
1393
+ </div>
1394
+ </div>`;
1395
+ }
1396
+
1397
+ function lessonSedimentationPrompt(task, candidate) {
1398
+ return [
1399
+ "You are executing a lesson sedimentation follow-up task.",
1400
+ "",
1401
+ `Source task: ${task.id}`,
1402
+ `Source candidate: ${candidate.id} - ${candidate.title || ""}`,
1403
+ `Candidate scope: ${candidate.scope || "unspecified"}`,
1404
+ `Candidate module key: ${candidate.moduleKey || "n/a"}`,
1405
+ `Detail artifact: ${candidate.detailArtifact || "not provided"}`,
1406
+ `Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
1407
+ `Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
1408
+ `Promotion target: ${candidate.promotionTarget || "unspecified"}`,
1409
+ `Conflict check: ${candidate.conflictCheck || "pending"}`,
1410
+ `Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
1411
+ "",
1412
+ "Instructions:",
1413
+ "1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
1414
+ "2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
1415
+ "3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
1416
+ "4. Check conflicts against existing lessons and standards.",
1417
+ "5. Propose the smallest diff first.",
1418
+ "6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
1419
+ ].join("\n");
1420
+ }
1421
+
1422
+ function reviewReason(reason) {
1423
+ return `<div class="review-reason">
1424
+ <strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
1425
+ <span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
1426
+ </div>`;
1427
+ }
1428
+
1006
1429
  function firstUsefulLine(text) {
1007
1430
  return String(text || "")
1008
1431
  .split(/\n+/)
@@ -1010,6 +1433,57 @@ function firstUsefulLine(text) {
1010
1433
  .filter(Boolean)[0] || "";
1011
1434
  }
1012
1435
 
1436
+ function reviewWorkspace(route) {
1437
+ const task = (bundle.status?.tasks || []).find((item) => item.id === route.id);
1438
+ if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
1439
+ const walkthroughDoc = taskDocument(task, "__walkthrough__");
1440
+ const candidateDoc = taskDocument(task, "lesson_candidates.md");
1441
+ const reviewDoc = taskDocument(task, "review.md");
1442
+ const findingsDoc = taskDocument(task, "findings.md");
1443
+ return `<main class="review-workspace">
1444
+ <nav class="crumbs"><a href="#/review">${t("reviewQueue")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
1445
+ <section class="detail-hero review-hero">
1446
+ <div>
1447
+ <p class="eyebrow">${t("reviewWorkspace")}</p>
1448
+ <h2>${escapeHtml(task.title)}</h2>
1449
+ <p>${escapeHtml(task.path)}</p>
1450
+ </div>
1451
+ <div class="review-hero-tags">
1452
+ ${tag(task.lifecycleState || "unknown")}
1453
+ ${tag(task.reviewStatus || "missing")}
1454
+ ${tag(task.lessonCandidateStatus || "missing")}
1455
+ </div>
1456
+ </section>
1457
+ <section class="review-workspace-grid">
1458
+ <article class="review-workspace-main stack">
1459
+ ${reviewDocPanel("walkthrough", walkthroughDoc, task.walkthroughPath)}
1460
+ ${reviewDocPanel("lessonCandidates", candidateDoc, task.lessonCandidatePath)}
1461
+ ${reviewDocPanel("review", reviewDoc, task.reviewPath)}
1462
+ ${reviewDocPanel("findings", findingsDoc, task.findingsPath)}
1463
+ </article>
1464
+ <aside class="review-workspace-side stack">
1465
+ ${reviewActionPanel(task, { mode: "workspace" })}
1466
+ ${taskStateSummary(task)}
1467
+ ${openFindings(task)}
1468
+ ${evidenceList(task)}
1469
+ </aside>
1470
+ </section>
1471
+ </main>`;
1472
+ }
1473
+
1474
+ function reviewDocPanel(key, doc, fallbackPath = "") {
1475
+ return `<section class="doc-section review-doc-panel">
1476
+ <div class="section-head">
1477
+ <div>
1478
+ <p class="eyebrow">${escapeHtml(fallbackPath || "")}</p>
1479
+ <h2>${t(key)}</h2>
1480
+ </div>
1481
+ ${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
1482
+ </div>
1483
+ <div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
1484
+ </section>`;
1485
+ }
1486
+
1013
1487
  function migrationPanel() {
1014
1488
  const advice = warningQueue();
1015
1489
  const missingBriefs = advice.filter((warning) => warning.type === "missing-brief").length;
@@ -1150,25 +1624,39 @@ function pager(kind, page, pageCount, group = "") {
1150
1624
  }
1151
1625
 
1152
1626
  function lessonPanel() {
1153
- const lessons = (bundle.tables?.tables || [])
1154
- .filter((table) => table.kind === "lessons-ssot")
1155
- .flatMap((table) => table.rows);
1627
+ const lessons = lessonDocuments();
1156
1628
  return `<section class="lesson-panel">
1157
1629
  <div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
1158
1630
  <div class="lesson-list" style="padding-top: 10px;">
1159
- ${lessons.map((row) => {
1160
- const cells = row.cells || {};
1161
- const lessonId = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
1162
- const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
1163
- return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lessonId)}">
1164
- <strong>${escapeHtml(lessonId)}</strong>
1165
- <p>${escapeHtml(summary)}</p>
1631
+ ${lessons.map((lesson) => {
1632
+ return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
1633
+ <strong>${escapeHtml(lesson.id)}</strong>
1634
+ <p>${escapeHtml(lesson.title || lesson.path)}</p>
1166
1635
  </div>`;
1167
1636
  }).join("") || emptyState(t("noLessons"))}
1168
1637
  </div>
1169
1638
  </section>`;
1170
1639
  }
1171
1640
 
1641
+ function lessonDocuments() {
1642
+ return (bundle.documents?.documents || [])
1643
+ .filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
1644
+ .map((doc) => {
1645
+ const id = lessonIdFromDocument(doc);
1646
+ return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
1647
+ })
1648
+ .filter((lesson) => lesson.id)
1649
+ .sort((left, right) => String(right.id).localeCompare(String(left.id)));
1650
+ }
1651
+
1652
+ function lessonIdFromDocument(doc) {
1653
+ const content = doc?.content || "";
1654
+ const path = doc?.path || "";
1655
+ return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
1656
+ || path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
1657
+ || "";
1658
+ }
1659
+
1172
1660
  function healthPanel() {
1173
1661
  const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
1174
1662
  return `<section class="health-panel">
@@ -1180,6 +1668,382 @@ function healthPanel() {
1180
1668
  </section>`;
1181
1669
  }
1182
1670
 
1671
+ function presetsView() {
1672
+ ensurePresetState();
1673
+ const catalog = bundle.presetCatalog || { summary: {}, roots: [], presets: [] };
1674
+ let presets = filteredPresets();
1675
+ syncVisiblePresetSelection(presets);
1676
+ presets = filteredPresets();
1677
+ const selected = selectedPreset(presets);
1678
+ syncPresetUninstallScope(selected);
1679
+ return `<div class="presets-page stack">
1680
+ <section class="flow-panel preset-command-center">
1681
+ <div class="section-head">
1682
+ <div>
1683
+ <p class="eyebrow">${t("presetCatalog")}</p>
1684
+ <h2>${t("presetCatalog")}</h2>
1685
+ <p class="subtle">${t("presetCatalogSubtitle")}</p>
1686
+ </div>
1687
+ <span class="preset-count-pill">${presets.length}/${catalog.summary?.total || 0}</span>
1688
+ </div>
1689
+ <div class="preset-priority-strip" aria-label="${escapeAttr(t("presetPriorityTitle"))}">
1690
+ ${presetPriorityStep("project", 1)}
1691
+ ${presetPriorityStep("user", 2)}
1692
+ ${presetPriorityStep("builtin", 3)}
1693
+ </div>
1694
+ <div class="preset-toolbar">
1695
+ <div class="input-group">
1696
+ <input data-preset-search value="${escapeAttr(state.presetQuery)}" placeholder="${escapeAttr(t("presetSearchPlaceholder"))}" aria-label="${escapeAttr(t("presetSearch"))}">
1697
+ </div>
1698
+ <div class="preset-source-tabs" role="tablist" aria-label="${escapeAttr(t("presetSourceFilter"))}">
1699
+ ${presetSourceOptions().map((source) => presetSourceButton(source)).join("")}
1700
+ </div>
1701
+ </div>
1702
+ </section>
1703
+ <section class="preset-workspace">
1704
+ <div class="flow-panel preset-collection-panel">
1705
+ <div class="preset-panel-heading">
1706
+ <div>
1707
+ <h3>${t("presetCollection")}</h3>
1708
+ <p>${t("presetCollectionHint")}</p>
1709
+ </div>
1710
+ </div>
1711
+ <div class="preset-catalog-list">
1712
+ ${presets.map((preset) => presetCard(preset, selected ? presetKey(selected) : "")).join("") || emptyState(t("noPresets"))}
1713
+ </div>
1714
+ </div>
1715
+ <div class="preset-detail-workspace stack">
1716
+ ${presetDetailPanel(selected)}
1717
+ ${presetLayerStackPanel(selected)}
1718
+ </div>
1719
+ <aside class="preset-context-actions stack">
1720
+ ${presetActionPanel(selected)}
1721
+ ${presetImportPanel()}
1722
+ ${presetRestorePanel()}
1723
+ ${presetSummaryPanel(catalog)}
1724
+ </aside>
1725
+ </section>
1726
+ </div>`;
1727
+ }
1728
+
1729
+ function ensurePresetState() {
1730
+ const presets = bundle.presetCatalog?.presets || [];
1731
+ if (!state.selectedPresetKey && state.selectedPresetId) {
1732
+ const legacySelection = presets.find((preset) => preset.id === state.selectedPresetId);
1733
+ if (legacySelection) state.selectedPresetKey = presetKey(legacySelection);
1734
+ }
1735
+ if (!state.selectedPresetKey && presets[0]) {
1736
+ state.selectedPresetKey = presetKey(presets[0]);
1737
+ state.presetUninstallConfirm = "";
1738
+ }
1739
+ if (state.selectedPresetKey && !presets.some((preset) => presetKey(preset) === state.selectedPresetKey) && presets[0]) {
1740
+ state.selectedPresetKey = presetKey(presets[0]);
1741
+ state.presetUninstallConfirm = "";
1742
+ }
1743
+ }
1744
+
1745
+ function presetSourceOptions() {
1746
+ return [
1747
+ ["all", t("allPresets")],
1748
+ ["project", t("presetSourceProject")],
1749
+ ["user", t("presetSourceUser")],
1750
+ ["builtin", t("presetSourceBuiltin")],
1751
+ ];
1752
+ }
1753
+
1754
+ function presetSourceButton([source, labelText]) {
1755
+ const active = state.presetSourceFilter === source;
1756
+ const count = source === "all" ? (bundle.presetCatalog?.summary?.total || 0) : (bundle.presetCatalog?.summary?.[source] || 0);
1757
+ return `<button type="button" class="${active ? "active" : ""}" data-preset-source-filter="${escapeAttr(source)}" role="tab" aria-selected="${active ? "true" : "false"}">
1758
+ <span>${escapeHtml(labelText)}</span>
1759
+ <strong>${count}</strong>
1760
+ </button>`;
1761
+ }
1762
+
1763
+ function filteredPresets() {
1764
+ const query = String(state.presetQuery || "").trim().toLowerCase();
1765
+ return (bundle.presetCatalog?.presets || []).filter((preset) => {
1766
+ if (state.presetSourceFilter !== "all" && preset.source !== state.presetSourceFilter) return false;
1767
+ return presetMatchesQuery(preset, query);
1768
+ });
1769
+ }
1770
+
1771
+ function presetMatchesQuery(preset, query = state.presetQuery) {
1772
+ const normalizedQuery = String(query || "").trim().toLowerCase();
1773
+ if (!normalizedQuery) return true;
1774
+ return [
1775
+ preset.id,
1776
+ preset.source,
1777
+ preset.purpose,
1778
+ preset.taskKind,
1779
+ preset.manifestPath,
1780
+ preset.version,
1781
+ ...(preset.compatibleBudgets || []),
1782
+ ].some((value) => String(value || "").toLowerCase().includes(normalizedQuery));
1783
+ }
1784
+
1785
+ function syncVisiblePresetSelection(visiblePresets) {
1786
+ if (!visiblePresets.length) {
1787
+ state.selectedPresetKey = "";
1788
+ state.presetUninstallConfirm = "";
1789
+ return;
1790
+ }
1791
+ if (!visiblePresets.some((preset) => presetKey(preset) === state.selectedPresetKey)) {
1792
+ state.selectedPresetKey = presetKey(visiblePresets[0]);
1793
+ state.presetUninstallConfirm = "";
1794
+ }
1795
+ }
1796
+
1797
+ function selectedPreset(visiblePresets = filteredPresets()) {
1798
+ return visiblePresets.find((preset) => presetKey(preset) === state.selectedPresetKey) || visiblePresets[0] || null;
1799
+ }
1800
+
1801
+ function presetCard(preset, selectedId) {
1802
+ const key = presetKey(preset);
1803
+ const selected = key === selectedId;
1804
+ return `<article class="preset-card ${selected ? "active" : ""} ${preset.effective ? "effective" : "shadowed"}">
1805
+ <div class="preset-card-topline">
1806
+ <button type="button" class="preset-card-select" data-preset-select="${escapeAttr(key)}" aria-pressed="${selected ? "true" : "false"}">
1807
+ <span class="card-id">${escapeHtml(preset.id)}</span>
1808
+ </button>
1809
+ <div class="preset-card-tools">
1810
+ ${presetSourceBadge(preset.source)}
1811
+ ${presetStatusBadge(preset)}
1812
+ <button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}" title="${escapeAttr(t("copyPresetId"))}">${t("copyIdShort")}</button>
1813
+ </div>
1814
+ </div>
1815
+ <button type="button" class="preset-card-body" data-preset-select="${escapeAttr(key)}">
1816
+ <span>${escapeHtml(preset.purpose || t("none"))}</span>
1817
+ </button>
1818
+ <div class="preset-card-meta">
1819
+ <span>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(preset))}</span>
1820
+ <span>${t("taskKind")}: ${escapeHtml(preset.taskKind || t("none"))}</span>
1821
+ <span>${t("budgets")}: ${escapeHtml((preset.compatibleBudgets || []).join(", ") || t("none"))}</span>
1822
+ </div>
1823
+ <code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
1824
+ </article>`;
1825
+ }
1826
+
1827
+ function presetKey(preset) {
1828
+ return preset?.key || `${preset?.source || "unknown"}:${preset?.id || ""}`;
1829
+ }
1830
+
1831
+ function presetSourceRank(source) {
1832
+ return { project: 1, user: 2, builtin: 3 }[source] || 9;
1833
+ }
1834
+
1835
+ function presetLayersForId(id) {
1836
+ return (bundle.presetCatalog?.presets || [])
1837
+ .filter((preset) => preset.id === id)
1838
+ .sort((a, b) => presetSourceRank(a.source) - presetSourceRank(b.source));
1839
+ }
1840
+
1841
+ function syncPresetUninstallScope(preset) {
1842
+ if (preset && ["project", "user"].includes(preset.source)) state.presetUninstallScope = preset.source;
1843
+ }
1844
+
1845
+ function presetPriorityStep(source, index) {
1846
+ return `<div class="preset-priority-step">
1847
+ <span>${index}</span>
1848
+ <strong>${escapeHtml(t(`presetSource_${source}`) || source)}</strong>
1849
+ </div>`;
1850
+ }
1851
+
1852
+ function presetSourceBadge(source) {
1853
+ const normalized = String(source || "unknown");
1854
+ return `<span class="tag preset-source-badge ${escapeAttr(normalized)}">${escapeHtml(t(`presetSource_${normalized}`) || normalized)}</span>`;
1855
+ }
1856
+
1857
+ function presetStatusBadge(preset) {
1858
+ return `<span class="tag ${preset.effective ? "pass" : "warn"}">${escapeHtml(preset.effective ? t("presetEffective") : t("presetShadowed"))}</span>`;
1859
+ }
1860
+
1861
+ function formatPresetVersion(preset) {
1862
+ return preset?.version ?? t("none");
1863
+ }
1864
+
1865
+ function presetSummaryPanel(catalog) {
1866
+ const roots = catalog.roots || [];
1867
+ return `<section class="side-panel preset-summary-panel">
1868
+ <h3>${t("presetSources")}</h3>
1869
+ <p class="preset-helper">${t("presetSourcesHint")}</p>
1870
+ <div class="metrics-grid compact">
1871
+ ${metric(t("presetSourceProject"), catalog.summary?.project || 0)}
1872
+ ${metric(t("presetSourceUser"), catalog.summary?.user || 0)}
1873
+ ${metric(t("presetSourceBuiltin"), catalog.summary?.builtin || 0)}
1874
+ </div>
1875
+ <div class="preset-roots">
1876
+ ${roots.map((root) => `<div><strong>${escapeHtml(t(`presetSource_${root.source}`) || root.source)}</strong><code>${escapeHtml(root.path || "")}</code></div>`).join("")}
1877
+ </div>
1878
+ </section>`;
1879
+ }
1880
+
1881
+ function presetDetailPanel(preset) {
1882
+ if (!preset) return `<section class="flow-panel preset-detail-panel">${emptyState(t("noPresets"))}</section>`;
1883
+ const inspectCommand = `harness preset inspect ${preset.id} --json .`;
1884
+ const checkCommand = `harness preset check ${preset.id} --json .`;
1885
+ const commandRows = preset.effective
1886
+ ? `${presetCommandRow(inspectCommand)}${presetCommandRow(checkCommand)}`
1887
+ : `<div class="preset-command-warning">${escapeHtml(t("presetCommandsEffectiveOnly"))}</div>`;
1888
+ return `<section class="flow-panel preset-detail-panel">
1889
+ <div class="preset-detail-hero">
1890
+ <div>
1891
+ <div class="preset-detail-title-row">
1892
+ <h3>${escapeHtml(preset.id)}</h3>
1893
+ <button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}">${t("copyPresetId")}</button>
1894
+ </div>
1895
+ <p>${escapeHtml(preset.purpose || "")}</p>
1896
+ </div>
1897
+ <div class="preset-detail-badges">
1898
+ ${presetSourceBadge(preset.source)}
1899
+ ${presetStatusBadge(preset)}
1900
+ </div>
1901
+ </div>
1902
+ <dl class="preset-detail-list">
1903
+ ${presetDetailRow(t("manifestVersion"), formatPresetVersion(preset))}
1904
+ ${presetDetailRow(t("source"), t(`presetSource_${preset.source}`) || preset.source)}
1905
+ ${presetDetailRow(t("status"), preset.effective ? t("presetEffective") : t("presetShadowed"))}
1906
+ ${presetDetailRow(t("taskKind"), preset.taskKind || t("none"))}
1907
+ ${presetDetailRow(t("budgets"), (preset.compatibleBudgets || []).join(", ") || t("none"))}
1908
+ ${presetDetailRow(t("inputs"), preset.inputCount || 0)}
1909
+ ${presetDetailRow(t("references"), preset.referenceCount || 0)}
1910
+ ${presetDetailRow(t("artifacts"), preset.artifactCount || 0)}
1911
+ ${presetDetailRow(t("writeScopes"), preset.writeScopeCount || 0)}
1912
+ ${presetDetailRow(t("requiredReads"), preset.requiredReadCount || 0)}
1913
+ </dl>
1914
+ <div class="preset-path-block">
1915
+ <span>${t("manifestPath")}</span>
1916
+ <code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
1917
+ </div>
1918
+ <div class="preset-command-list">
1919
+ ${commandRows}
1920
+ </div>
1921
+ </section>`;
1922
+ }
1923
+
1924
+ function presetDetailRow(labelText, value) {
1925
+ return `<div><dt>${escapeHtml(labelText)}</dt><dd>${escapeHtml(String(value ?? ""))}</dd></div>`;
1926
+ }
1927
+
1928
+ function presetCommandRow(command) {
1929
+ return `<div class="preset-command-row">
1930
+ <code>${escapeHtml(command)}</code>
1931
+ <button type="button" class="copy-inline" data-copy-preset-command="${escapeAttr(command)}">${t("copyCommand")}</button>
1932
+ </div>`;
1933
+ }
1934
+
1935
+ function presetLayerStackPanel(preset) {
1936
+ if (!preset) return "";
1937
+ const layers = presetLayersForId(preset.id);
1938
+ return `<section class="flow-panel preset-layer-panel">
1939
+ <div class="preset-panel-heading">
1940
+ <div>
1941
+ <h3>${t("presetLayerStack")}</h3>
1942
+ <p>${t("presetLayerStackHint")}</p>
1943
+ </div>
1944
+ </div>
1945
+ <div class="preset-layer-list">
1946
+ ${layers.map((layer) => `<button type="button" class="preset-layer-row ${presetKey(layer) === presetKey(preset) ? "active" : ""}" data-preset-select="${escapeAttr(presetKey(layer))}">
1947
+ <span class="preset-layer-rank">${presetSourceRank(layer.source)}</span>
1948
+ <span>
1949
+ <strong>${escapeHtml(t(`presetSource_${layer.source}`) || layer.source)}</strong>
1950
+ <small>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(layer))}</small>
1951
+ </span>
1952
+ ${presetStatusBadge(layer)}
1953
+ </button>`).join("")}
1954
+ </div>
1955
+ </section>`;
1956
+ }
1957
+
1958
+ function presetActionPanel(preset) {
1959
+ const staticNote = canUseWorkbenchAction("preset-install") ? "" : `<p class="lesson-action-note">${escapeHtml(t("presetWorkbenchRequired"))}</p>`;
1960
+ const lockedUninstallScope = preset && ["project", "user"].includes(preset.source) ? preset.source : "";
1961
+ const confirmMatches = Boolean(preset && state.presetUninstallConfirm.trim() === preset.id);
1962
+ const canCheck = canUseWorkbenchAction("preset-check") && preset && preset.effective;
1963
+ const canUninstall = canUseWorkbenchAction("preset-uninstall") && preset && preset.source !== "builtin" && confirmMatches;
1964
+ return `<section class="side-panel preset-action-panel">
1965
+ <div class="preset-panel-heading">
1966
+ <div>
1967
+ <h3>${t("presetContextActions")}</h3>
1968
+ <p>${preset ? escapeHtml(preset.id) : t("noPresets")}</p>
1969
+ </div>
1970
+ </div>
1971
+ ${staticNote}
1972
+ ${presetActionResult()}
1973
+ <div class="preset-action-group">
1974
+ <h4>${t("presetCheck")}</h4>
1975
+ <p>${preset?.effective ? t("presetCheckHint") : t("presetShadowedActionHint")}</p>
1976
+ <button data-preset-check="${escapeAttr(preset?.id || "")}" ${canCheck ? "" : "disabled"}>${t("presetCheckSelected")}</button>
1977
+ </div>
1978
+ <div class="preset-action-group danger">
1979
+ <h4>${t("presetUninstallSelected")}</h4>
1980
+ <p>${preset?.source === "builtin" ? t("presetBuiltinImmutable") : t("presetUninstallHint")}</p>
1981
+ <label>${t("scope")}<select data-preset-uninstall-scope ${lockedUninstallScope ? "disabled" : ""}>
1982
+ ${presetScopeOptions(lockedUninstallScope || state.presetUninstallScope)}
1983
+ </select></label>
1984
+ <div class="preset-confirm-row">
1985
+ <label>${t("confirmPresetId")}<input data-preset-uninstall-confirm value="${escapeAttr(state.presetUninstallConfirm)}" placeholder="${escapeAttr(preset?.id || "")}"></label>
1986
+ <button type="button" data-preset-fill-confirm="${escapeAttr(preset?.id || "")}" ${preset && preset.source !== "builtin" ? "" : "disabled"}>${t("useSelectedId")}</button>
1987
+ </div>
1988
+ ${preset && preset.source !== "builtin" && !confirmMatches ? `<p class="preset-confirm-warning">${escapeHtml(t("presetConfirmRequired"))}</p>` : ""}
1989
+ <button data-preset-uninstall="${escapeAttr(preset?.id || "")}" ${canUninstall ? "" : "disabled"}>${t("presetUninstallSelected")}</button>
1990
+ </div>
1991
+ </section>`;
1992
+ }
1993
+
1994
+ function presetImportPanel() {
1995
+ return `<section class="side-panel preset-action-panel">
1996
+ <div class="preset-panel-heading">
1997
+ <div>
1998
+ <h3>${t("presetImportTitle")}</h3>
1999
+ <p>${t("presetImportHint")}</p>
2000
+ </div>
2001
+ </div>
2002
+ <div class="preset-action-group">
2003
+ <label>${t("source")}<input data-preset-install-source value="${escapeAttr(state.presetInstallSource)}" placeholder="${escapeAttr(t("presetInstallSourcePlaceholder"))}"></label>
2004
+ <label>${t("scope")}<select data-preset-install-scope>
2005
+ ${presetScopeOptions(state.presetInstallScope)}
2006
+ </select></label>
2007
+ <label class="check-row"><input type="checkbox" data-preset-install-force ${state.presetInstallForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
2008
+ <button data-preset-install ${canUseWorkbenchAction("preset-install") ? "" : "disabled"}>${t("presetInstall")}</button>
2009
+ </div>
2010
+ </section>`;
2011
+ }
2012
+
2013
+ function presetRestorePanel() {
2014
+ return `<section class="side-panel preset-action-panel">
2015
+ <div class="preset-panel-heading">
2016
+ <div>
2017
+ <h3>${t("presetRestoreBundled")}</h3>
2018
+ <p>${t("presetRestoreBundledHint")}</p>
2019
+ </div>
2020
+ </div>
2021
+ <div class="preset-action-group">
2022
+ <label>${t("scope")}<select data-preset-seed-scope>
2023
+ ${presetScopeOptions(state.presetSeedScope)}
2024
+ </select></label>
2025
+ <label class="check-row"><input type="checkbox" data-preset-seed-force ${state.presetSeedForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
2026
+ <button data-preset-seed ${canUseWorkbenchAction("preset-seed") ? "" : "disabled"}>${t("presetRestoreBundled")}</button>
2027
+ </div>
2028
+ </section>`;
2029
+ }
2030
+
2031
+ function presetScopeOptions(current) {
2032
+ return [["project", t("presetSourceProject")], ["user", t("presetSourceUser")]]
2033
+ .map(([value, labelText]) => `<option value="${value}" ${current === value ? "selected" : ""}>${escapeHtml(labelText)}</option>`)
2034
+ .join("");
2035
+ }
2036
+
2037
+ function presetActionResult() {
2038
+ const result = state.presetActionResult;
2039
+ if (!result) return "";
2040
+ const klass = result.ok ? "success" : "failed";
2041
+ return `<div class="workbench-action-result ${klass}">
2042
+ <strong>${escapeHtml(result.title || "")}</strong>
2043
+ <span>${escapeHtml(result.message || "")}</span>
2044
+ </div>`;
2045
+ }
2046
+
1183
2047
  function taskDocument(task, fileName) {
1184
2048
  if (fileName === "__walkthrough__" && task.walkthroughPath) return findDocument(task.walkthroughPath);
1185
2049
  return findDocument(`${task.path}/${fileName}`);
@@ -1210,7 +2074,9 @@ function tag(value) {
1210
2074
  }
1211
2075
 
1212
2076
  function label(value) {
1213
- return t(`state_${value}`) || String(value || "unknown").replaceAll("_", " ");
2077
+ const key = `state_${value}`;
2078
+ const translated = t(key);
2079
+ return translated === key ? String(value || "unknown").replaceAll("_", " ") : translated;
1214
2080
  }
1215
2081
 
1216
2082
  function list(items = []) {
@@ -1272,6 +2138,13 @@ function bind() {
1272
2138
  localStorage.setItem("harness.taskLayout", state.taskLayout);
1273
2139
  app();
1274
2140
  }));
2141
+ document.querySelectorAll("[data-task-sort-order]").forEach((btn) => btn.addEventListener("click", () => {
2142
+ state.taskSortOrder = btn.dataset.taskSortOrder === "asc" ? "asc" : "desc";
2143
+ localStorage.setItem("harness.taskSortOrder", state.taskSortOrder);
2144
+ state.taskPageByGroup = {};
2145
+ state.taskGroupPage = 1;
2146
+ app();
2147
+ }));
1275
2148
  document.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
1276
2149
  state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
1277
2150
  app();
@@ -1286,11 +2159,89 @@ function bind() {
1286
2159
  state.warningPage = 1;
1287
2160
  app();
1288
2161
  }));
2162
+ document.querySelectorAll("[data-preset-search]").forEach((input) => input.addEventListener("input", () => {
2163
+ state.presetQuery = input.value;
2164
+ app();
2165
+ }));
2166
+ document.querySelectorAll("[data-preset-source-filter]").forEach((button) => button.addEventListener("click", () => {
2167
+ state.presetSourceFilter = button.dataset.presetSourceFilter || "all";
2168
+ state.selectedPresetKey = "";
2169
+ state.presetUninstallConfirm = "";
2170
+ app();
2171
+ }));
2172
+ document.querySelectorAll("[data-preset-select]").forEach((button) => button.addEventListener("click", () => {
2173
+ state.selectedPresetKey = button.dataset.presetSelect || "";
2174
+ state.selectedPresetId = "";
2175
+ const selectedPreset = (bundle.presetCatalog?.presets || []).find((preset) => presetKey(preset) === state.selectedPresetKey);
2176
+ if (selectedPreset && state.presetSourceFilter !== "all" && selectedPreset.source !== state.presetSourceFilter) {
2177
+ state.presetSourceFilter = selectedPreset.source;
2178
+ }
2179
+ if (selectedPreset && !presetMatchesQuery(selectedPreset)) state.presetQuery = "";
2180
+ if (selectedPreset && ["project", "user"].includes(selectedPreset.source)) state.presetUninstallScope = selectedPreset.source;
2181
+ state.presetUninstallConfirm = "";
2182
+ app();
2183
+ }));
2184
+ document.querySelectorAll("[data-preset-install-source]").forEach((input) => input.addEventListener("input", () => {
2185
+ state.presetInstallSource = input.value;
2186
+ }));
2187
+ document.querySelectorAll("[data-preset-install-scope]").forEach((select) => select.addEventListener("change", () => {
2188
+ state.presetInstallScope = select.value || "project";
2189
+ }));
2190
+ document.querySelectorAll("[data-preset-install-force]").forEach((input) => input.addEventListener("change", () => {
2191
+ state.presetInstallForce = input.checked;
2192
+ }));
2193
+ document.querySelectorAll("[data-preset-seed-scope]").forEach((select) => select.addEventListener("change", () => {
2194
+ state.presetSeedScope = select.value || "project";
2195
+ }));
2196
+ document.querySelectorAll("[data-preset-seed-force]").forEach((input) => input.addEventListener("change", () => {
2197
+ state.presetSeedForce = input.checked;
2198
+ }));
2199
+ document.querySelectorAll("[data-preset-uninstall-scope]").forEach((select) => select.addEventListener("change", () => {
2200
+ state.presetUninstallScope = select.value || "project";
2201
+ }));
2202
+ document.querySelectorAll("[data-preset-uninstall-confirm]").forEach((input) => input.addEventListener("input", () => {
2203
+ state.presetUninstallConfirm = input.value;
2204
+ }));
2205
+ document.querySelectorAll("[data-preset-fill-confirm]").forEach((button) => button.addEventListener("click", () => {
2206
+ state.presetUninstallConfirm = button.dataset.presetFillConfirm || "";
2207
+ app();
2208
+ }));
2209
+ document.querySelectorAll("[data-preset-check]").forEach((button) => button.addEventListener("click", () => runPresetAction("check", { id: button.dataset.presetCheck || "" })));
2210
+ document.querySelectorAll("[data-preset-install]").forEach((button) => button.addEventListener("click", () => runPresetAction("install", {
2211
+ source: state.presetInstallSource,
2212
+ scope: state.presetInstallScope,
2213
+ force: state.presetInstallForce,
2214
+ })));
2215
+ document.querySelectorAll("[data-preset-seed]").forEach((button) => button.addEventListener("click", () => runPresetAction("seed", {
2216
+ scope: state.presetSeedScope,
2217
+ force: state.presetSeedForce,
2218
+ })));
2219
+ document.querySelectorAll("[data-preset-uninstall]").forEach((button) => button.addEventListener("click", () => runPresetAction("uninstall", {
2220
+ id: button.dataset.presetUninstall || "",
2221
+ scope: state.presetUninstallScope,
2222
+ confirmText: state.presetUninstallConfirm,
2223
+ })));
2224
+ document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
2225
+ state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
2226
+ state.reviewQueuePage = 1;
2227
+ app();
2228
+ }));
2229
+ document.querySelectorAll("[data-review-reason-filter]").forEach((select) => select.addEventListener("change", () => {
2230
+ state.reviewReasonFilter = select.value || "all";
2231
+ state.reviewQueuePage = 1;
2232
+ app();
2233
+ }));
2234
+ document.querySelectorAll("[data-review-sort]").forEach((select) => select.addEventListener("change", () => {
2235
+ state.reviewSort = select.value || "queue";
2236
+ state.reviewQueuePage = 1;
2237
+ app();
2238
+ }));
1289
2239
  document.querySelectorAll("[data-page-kind]").forEach((button) => button.addEventListener("click", () => {
1290
2240
  const page = Math.max(1, Number(button.dataset.page) || 1);
1291
2241
  if (button.dataset.pageKind === "warning") state.warningPage = page;
1292
2242
  if (button.dataset.pageKind === "task-groups") state.taskGroupPage = page;
1293
2243
  if (button.dataset.pageKind === "task") state.taskPageByGroup[button.dataset.pageGroup || ""] = page;
2244
+ if (button.dataset.pageKind === "review") state.reviewQueuePage = page;
1294
2245
  app();
1295
2246
  }));
1296
2247
  document.querySelectorAll("[data-runway-phase]").forEach((link) => link.addEventListener("click", () => {
@@ -1315,6 +2266,10 @@ function bind() {
1315
2266
  const taskId = el.dataset.openDrawer;
1316
2267
  openDrawer(taskId);
1317
2268
  }));
2269
+ bindCopyTaskNameButtons(document);
2270
+ bindPresetCopyButtons(document);
2271
+ bindRepairPromptButtons(document);
2272
+ bindLessonSedimentationButtons(document);
1318
2273
  document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
1319
2274
  e.preventDefault();
1320
2275
  const lessonId = el.dataset.openLessonDrawer;
@@ -1395,6 +2350,45 @@ async function completeReviewFromDashboard(taskId) {
1395
2350
  }
1396
2351
  }
1397
2352
 
2353
+ async function runPresetAction(action, body) {
2354
+ state.presetActionResult = { ok: true, title: t("presetActionRunning"), message: action };
2355
+ app();
2356
+ try {
2357
+ const response = await fetch(`/api/presets/${action}`, {
2358
+ method: "POST",
2359
+ headers: {
2360
+ "content-type": "application/json",
2361
+ "x-harness-csrf": state.runtime?.csrfToken || "",
2362
+ },
2363
+ body: JSON.stringify(body),
2364
+ });
2365
+ const payload = await response.json();
2366
+ if (!response.ok) throw payload;
2367
+ state.presetActionResult = {
2368
+ ok: true,
2369
+ title: t("presetActionSuccess"),
2370
+ message: presetActionMessage(action, payload),
2371
+ };
2372
+ app();
2373
+ if (["install", "seed", "uninstall"].includes(action)) setTimeout(() => window.location.reload(), 650);
2374
+ } catch (error) {
2375
+ state.presetActionResult = {
2376
+ ok: false,
2377
+ title: t("presetActionFailed"),
2378
+ message: error?.error || error?.message || String(error || action),
2379
+ };
2380
+ app();
2381
+ }
2382
+ }
2383
+
2384
+ function presetActionMessage(action, payload) {
2385
+ if (action === "check") return `${payload.id || ""} ${payload.status || ""}`.trim();
2386
+ if (action === "install") return `${payload.id || ""} -> ${payload.scope || ""}`.trim();
2387
+ if (action === "seed") return `${payload.created || 0} ${t("created")} · ${payload.skipped || 0} ${t("skipped")}`;
2388
+ if (action === "uninstall") return `${payload.id || ""} ${payload.removed ? t("removed") : t("notInstalled")}`.trim();
2389
+ return action;
2390
+ }
2391
+
1398
2392
  function renderDrawerContent(taskId) {
1399
2393
  const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
1400
2394
  if (!task) return `<div class="empty">${t("taskNotFound")}</div>`;
@@ -1404,6 +2398,7 @@ function renderDrawerContent(taskId) {
1404
2398
  <div>
1405
2399
  <h2>${escapeHtml(task.title)}</h2>
1406
2400
  <p style="font-family: var(--font-mono); font-size: 11px; margin: 4px 0 0; color: var(--muted);">${escapeHtml(task.id)}</p>
2401
+ ${taskCopyButton(task, "detail-copy")}
1407
2402
  </div>
1408
2403
  <button class="btn-close" data-close-drawer>×</button>
1409
2404
  </div>
@@ -1416,12 +2411,16 @@ function renderDrawerContent(taskId) {
1416
2411
 
1417
2412
  const body = `
1418
2413
  <div class="task-drawer-body stack">
1419
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: var(--paper-2); padding: 12px 16px; border-radius: 8px;">
1420
- <div style="font-size: 24px; font-weight: 800; color: var(--accent);">${task.completion}%</div>
1421
- <a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger" style="text-decoration: none;">${t("fullView")}</a>
2414
+ <div class="drawer-task-summary">
2415
+ <div>
2416
+ <span>${t("statOverall")}</span>
2417
+ <strong>${task.completion}%</strong>
2418
+ </div>
2419
+ <a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger">${t("fullView")}</a>
1422
2420
  </div>
1423
2421
  ${taskStateSummary(task)}
1424
- ${reviewActionPanel(task)}
2422
+ ${reviewActionPanel(task, { mode: "summary" })}
2423
+ ${lessonCandidatePanel(task, { context: "drawer" })}
1425
2424
  ${timeline}
1426
2425
  ${documents}
1427
2426
  ${findings}
@@ -1445,18 +2444,174 @@ function openDrawer(taskId) {
1445
2444
  state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
1446
2445
  openDrawer(taskId);
1447
2446
  }));
2447
+ bindCopyTaskNameButtons(drawer);
2448
+ bindRepairPromptButtons(drawer);
2449
+ bindLessonSedimentationButtons(drawer);
1448
2450
  drawer.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
1449
2451
  }
1450
2452
 
2453
+ function bindCopyTaskNameButtons(root) {
2454
+ root.querySelectorAll("[data-copy-task-name]").forEach((button) => button.addEventListener("click", async (event) => {
2455
+ event.preventDefault();
2456
+ event.stopPropagation();
2457
+ const taskName = button.dataset.copyTaskName || "";
2458
+ const defaultText = t("copyTaskNameShort");
2459
+ try {
2460
+ await copyText(taskName);
2461
+ button.textContent = t("copyTaskNameSuccess");
2462
+ } catch {
2463
+ button.textContent = t("copyTaskNameFailed");
2464
+ }
2465
+ window.setTimeout(() => {
2466
+ button.textContent = defaultText;
2467
+ }, 1400);
2468
+ }));
2469
+ }
2470
+
2471
+ function bindPresetCopyButtons(root) {
2472
+ root.querySelectorAll("[data-copy-preset-id]").forEach((button) => button.addEventListener("click", async (event) => {
2473
+ event.preventDefault();
2474
+ event.stopPropagation();
2475
+ const presetId = button.dataset.copyPresetId || "";
2476
+ const defaultText = button.textContent;
2477
+ try {
2478
+ await copyText(presetId);
2479
+ button.textContent = t("copyTaskNameSuccess");
2480
+ } catch {
2481
+ button.textContent = t("copyTaskNameFailed");
2482
+ }
2483
+ setTimeout(() => { button.textContent = defaultText; }, 1200);
2484
+ }));
2485
+ root.querySelectorAll("[data-copy-preset-command]").forEach((button) => button.addEventListener("click", async (event) => {
2486
+ event.preventDefault();
2487
+ event.stopPropagation();
2488
+ const command = button.dataset.copyPresetCommand || "";
2489
+ const defaultText = button.textContent;
2490
+ try {
2491
+ await copyText(command);
2492
+ button.textContent = t("copyTaskNameSuccess");
2493
+ } catch {
2494
+ button.textContent = t("copyTaskNameFailed");
2495
+ }
2496
+ setTimeout(() => { button.textContent = defaultText; }, 1200);
2497
+ }));
2498
+ }
2499
+
2500
+ function bindRepairPromptButtons(root) {
2501
+ root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
2502
+ event.preventDefault();
2503
+ event.stopPropagation();
2504
+ const prompt = button.dataset.repairPrompt || "";
2505
+ const defaultText = t("copyRepairPrompt");
2506
+ try {
2507
+ await copyText(prompt);
2508
+ button.textContent = t("copyRepairPromptSuccess");
2509
+ } catch {
2510
+ button.textContent = t("copyTaskNameFailed");
2511
+ }
2512
+ window.setTimeout(() => {
2513
+ button.textContent = defaultText;
2514
+ }, 1400);
2515
+ }));
2516
+ }
2517
+
2518
+ function bindLessonSedimentationButtons(root) {
2519
+ root.querySelectorAll("[data-copy-lesson-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
2520
+ event.preventDefault();
2521
+ event.stopPropagation();
2522
+ const prompt = button.dataset.lessonPrompt || "";
2523
+ const defaultText = t("copyLessonPrompt");
2524
+ try {
2525
+ await copyText(prompt);
2526
+ button.textContent = t("copyRepairPromptSuccess");
2527
+ } catch {
2528
+ button.textContent = t("copyTaskNameFailed");
2529
+ }
2530
+ window.setTimeout(() => {
2531
+ button.textContent = defaultText;
2532
+ }, 1400);
2533
+ }));
2534
+ root.querySelectorAll("[data-create-lesson-sedimentation]").forEach((button) => button.addEventListener("click", async (event) => {
2535
+ event.preventDefault();
2536
+ event.stopPropagation();
2537
+ await createLessonSedimentationFromDashboard(button);
2538
+ }));
2539
+ }
2540
+
2541
+ async function createLessonSedimentationFromDashboard(button) {
2542
+ const taskId = button.dataset.createLessonSedimentation || "";
2543
+ const candidateId = button.dataset.candidateId || "";
2544
+ const result = document.querySelector(`[data-lesson-result="${CSS.escape(`${taskId}:${candidateId}`)}"]`);
2545
+ if (result) result.textContent = t("lessonTaskCreating");
2546
+ button.disabled = true;
2547
+ try {
2548
+ const response = await fetch("/api/tasks/lesson-sedimentation", {
2549
+ method: "POST",
2550
+ headers: {
2551
+ "content-type": "application/json",
2552
+ "x-harness-csrf": state.runtime?.csrfToken || "",
2553
+ },
2554
+ body: JSON.stringify({ taskId, candidateId }),
2555
+ });
2556
+ const payload = await response.json();
2557
+ if (!response.ok) throw payload;
2558
+ if (result) {
2559
+ result.innerHTML = lessonSedimentationSuccess(payload);
2560
+ bindLessonSedimentationButtons(result);
2561
+ result.scrollIntoView({ block: "center", inline: "nearest" });
2562
+ }
2563
+ } catch (error) {
2564
+ button.disabled = false;
2565
+ if (result) result.innerHTML = lessonSedimentationFailure(error);
2566
+ }
2567
+ }
2568
+
2569
+ function lessonSedimentationSuccess(payload) {
2570
+ const followUp = payload?.followUpTask || {};
2571
+ const prompt = payload?.prompt || "";
2572
+ const taskId = followUp.id || "";
2573
+ const openHref = taskId ? `#/tasks/${encodeURIComponent(taskId)}` : "#/review";
2574
+ return `<div class="workbench-action-result success">
2575
+ <strong>${escapeHtml(t("lessonTaskCreated"))}</strong>
2576
+ ${taskId ? `<a href="${openHref}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
2577
+ ${prompt ? `<button data-copy-lesson-prompt="${escapeAttr(taskId || "follow-up")}" data-lesson-prompt="${escapeAttr(prompt)}">${escapeHtml(t("copyLessonPrompt"))}</button>` : ""}
2578
+ </div>`;
2579
+ }
2580
+
2581
+ function lessonSedimentationFailure(error) {
2582
+ const message = error?.error || error?.message || t("lessonTaskCreateFailed");
2583
+ const recovery = Array.isArray(error?.recovery) ? error.recovery : [];
2584
+ const details = error?.details || {};
2585
+ const existingTask = details.followUpTask || details.existingTask || "";
2586
+ return `<div class="workbench-action-result failed">
2587
+ <strong>${escapeHtml(t("lessonTaskCreateFailed"))}</strong>
2588
+ <span>${escapeHtml(message)}</span>
2589
+ ${existingTask ? `<a href="#/tasks/${encodeURIComponent(existingTask)}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
2590
+ ${recovery.length ? `<ul>${recovery.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : ""}
2591
+ </div>`;
2592
+ }
2593
+
2594
+ async function copyText(text) {
2595
+ if (navigator.clipboard?.writeText) {
2596
+ await navigator.clipboard.writeText(text);
2597
+ return;
2598
+ }
2599
+ const textarea = document.createElement("textarea");
2600
+ textarea.value = text;
2601
+ textarea.setAttribute("readonly", "");
2602
+ textarea.style.position = "fixed";
2603
+ textarea.style.left = "-9999px";
2604
+ document.body.appendChild(textarea);
2605
+ textarea.select();
2606
+ const copied = document.execCommand("copy");
2607
+ textarea.remove();
2608
+ if (!copied) throw new Error("copy failed");
2609
+ }
2610
+
1451
2611
  function renderLessonDrawerContent(lessonId) {
1452
- const lessonTable = (bundle.tables?.tables || []).find((table) => table.kind === "lessons-ssot");
1453
- const row = (lessonTable?.rows || []).find((r) => {
1454
- const cells = r.cells || {};
1455
- const id = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
1456
- return id === lessonId;
1457
- });
2612
+ const lesson = lessonDocuments().find((item) => item.id === lessonId);
1458
2613
 
1459
- if (!row) {
2614
+ if (!lesson) {
1460
2615
  return `<div class="task-drawer-header">
1461
2616
  <h2>${escapeHtml(lessonId)}</h2>
1462
2617
  <button class="btn-close" data-close-drawer>×</button>
@@ -1466,23 +2621,13 @@ function renderLessonDrawerContent(lessonId) {
1466
2621
  </div>`;
1467
2622
  }
1468
2623
 
1469
- const cells = row.cells || {};
1470
- const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
1471
- const docPath = cells["\u8be6\u60c5\u6587\u6863"] || cells.Document || cells.document || "";
1472
-
1473
- let doc = null;
1474
- if (docPath) {
1475
- doc = findDocument(docPath);
1476
- }
1477
- if (!doc) {
1478
- doc = (bundle.documents?.documents || []).find((d) => d.path.includes(lessonId) || d.path.endsWith(`${lessonId}.md`));
1479
- }
2624
+ const doc = lesson.doc || findDocument(lesson.path);
1480
2625
 
1481
2626
  const header = `
1482
2627
  <div class="task-drawer-header">
1483
2628
  <div>
1484
2629
  <h2>${escapeHtml(lessonId)}</h2>
1485
- <p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(summary)}</p>
2630
+ <p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(lesson.title || lesson.path)}</p>
1486
2631
  </div>
1487
2632
  <button class="btn-close" data-close-drawer>×</button>
1488
2633
  </div>
@@ -1492,16 +2637,10 @@ function renderLessonDrawerContent(lessonId) {
1492
2637
  if (doc && doc.content) {
1493
2638
  markdownBody = `<div class="markdown">${window.HarnessMarkdown.render(doc.content, "rendered")}</div>`;
1494
2639
  } else {
1495
- const rowsHtml = Object.entries(cells)
1496
- .map(([key, val]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(val)}</td></tr>`)
1497
- .join("");
1498
2640
  markdownBody = `
1499
2641
  <div style="margin-bottom: 20px; background: var(--paper-2); padding: 16px; border-radius: 8px; border: 1px dashed var(--line);">
1500
2642
  <p style="margin: 0; font-size: 13px; color: var(--muted);">${t("lessonDocMissing")}</p>
1501
2643
  </div>
1502
- <table class="rendered-table" style="width: 100%;">
1503
- <tbody>${rowsHtml}</tbody>
1504
- </table>
1505
2644
  `;
1506
2645
  }
1507
2646