coding-agent-harness 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.md +211 -86
  4. package/README.zh-CN.md +54 -34
  5. package/SKILL.md +25 -18
  6. package/docs-release/README.md +9 -5
  7. package/docs-release/architecture/overview.md +17 -5
  8. package/docs-release/architecture/overview.zh-CN.md +9 -5
  9. package/docs-release/assets/dashboard-overview.png +0 -0
  10. package/docs-release/guides/agent-installation.en-US.md +31 -8
  11. package/docs-release/guides/agent-installation.md +34 -9
  12. package/docs-release/guides/contributing.md +100 -0
  13. package/docs-release/guides/contributing.zh-CN.md +99 -0
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  15. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  20. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  21. package/docs-release/guides/migration-playbook.md +14 -15
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  23. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  24. package/docs-release/guides/preset-development.md +214 -0
  25. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  26. package/docs-release/guides/repository-operating-models.md +5 -4
  27. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  28. package/docs-release/guides/task-state-machine.md +214 -0
  29. package/docs-release/intl/en-US.md +1 -1
  30. package/docs-release/intl/zh-CN.md +1 -1
  31. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  32. package/package.json +8 -3
  33. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  34. package/presets/legacy-migration/preset.yaml +134 -0
  35. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  36. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  37. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  38. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  39. package/presets/legacy-migration/templates/review.seed.md +12 -0
  40. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  41. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  42. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  43. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  44. package/presets/lesson-sedimentation/preset.yaml +23 -0
  45. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  46. package/presets/module/preset.yaml +25 -0
  47. package/presets/module/templates/execution_strategy.append.md +8 -0
  48. package/presets/module/templates/task_plan.append.md +17 -0
  49. package/presets/standard-task/preset.yaml +31 -0
  50. package/presets/standard-task/templates/task_plan.append.md +7 -0
  51. package/references/adversarial-review-standard.md +2 -2
  52. package/references/agents-md-pattern.md +2 -2
  53. package/references/delivery-operating-model-standard.md +3 -3
  54. package/references/docs-directory-standard.md +6 -7
  55. package/references/harness-ledger.md +53 -96
  56. package/references/lessons-governance.md +88 -93
  57. package/references/module-parallel-standard.md +14 -14
  58. package/references/planning-loop.md +12 -6
  59. package/references/pull-request-standard.md +118 -0
  60. package/references/repo-governance-standard.md +11 -2
  61. package/references/review-routing-standard.md +7 -1
  62. package/references/ssot-governance.md +67 -59
  63. package/references/taskr-gap-analysis.md +600 -0
  64. package/references/walkthrough-closeout.md +7 -7
  65. package/scripts/check-harness.mjs +40 -301
  66. package/scripts/commands/dashboard-command.mjs +67 -0
  67. package/scripts/commands/migration-command.mjs +96 -0
  68. package/scripts/commands/preset-command.mjs +73 -0
  69. package/scripts/commands/task-command.mjs +327 -0
  70. package/scripts/harness.mjs +55 -260
  71. package/scripts/lib/capability-registry.mjs +66 -8
  72. package/scripts/lib/check-module-parallel.mjs +237 -0
  73. package/scripts/lib/check-profiles.mjs +61 -153
  74. package/scripts/lib/check-task-contracts.mjs +47 -0
  75. package/scripts/lib/core-shared.mjs +10 -0
  76. package/scripts/lib/dashboard-data.mjs +29 -6
  77. package/scripts/lib/dashboard-workbench.mjs +52 -12
  78. package/scripts/lib/dashboard-writer.mjs +14 -2
  79. package/scripts/lib/git-status-summary.mjs +46 -0
  80. package/scripts/lib/governance-index-generator.mjs +174 -0
  81. package/scripts/lib/governance-sync.mjs +514 -0
  82. package/scripts/lib/governance-table-boundary.mjs +175 -0
  83. package/scripts/lib/harness-core.mjs +5 -0
  84. package/scripts/lib/lesson-maintenance.mjs +36 -29
  85. package/scripts/lib/migration-support.mjs +1 -1
  86. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  87. package/scripts/lib/preset-engine.mjs +497 -0
  88. package/scripts/lib/preset-registry.mjs +627 -0
  89. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  90. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  91. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  92. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  93. package/scripts/lib/task-completion-consistency.mjs +16 -0
  94. package/scripts/lib/task-index.mjs +93 -0
  95. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  96. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  97. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  98. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  99. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  100. package/scripts/lib/task-lifecycle.mjs +297 -403
  101. package/scripts/lib/task-review-model.mjs +469 -0
  102. package/scripts/lib/task-scanner.mjs +130 -236
  103. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  104. package/scripts/postinstall.mjs +14 -0
  105. package/skills/preset-creator/SKILL.md +179 -0
  106. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  107. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  108. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  109. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  110. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  111. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  112. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  113. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  114. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  115. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  116. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  117. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  118. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  119. package/templates/AGENTS.md.template +19 -15
  120. package/templates/dashboard/assets/app-src/00-state.js +1 -0
  121. package/templates/dashboard/assets/app-src/10-router.js +2 -1
  122. package/templates/dashboard/assets/app-src/20-overview.js +11 -5
  123. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  124. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  125. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  126. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  127. package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
  128. package/templates/dashboard/assets/app.css +698 -156
  129. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  130. package/templates/dashboard/assets/app.js +662 -91
  131. package/templates/dashboard/assets/app.manifest.json +1 -0
  132. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  133. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  134. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  135. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  136. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  137. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  138. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  139. package/templates/dashboard/assets/i18n.js +123 -21
  140. package/templates/ledger/Harness-Ledger.md +13 -25
  141. package/templates/lessons/lesson-arch-process-change.md +1 -1
  142. package/templates/lessons/lesson-new-doc.md +1 -1
  143. package/templates/lessons/lesson-ref-change.md +1 -1
  144. package/templates/planning/execution_strategy.md +31 -0
  145. package/templates/planning/lesson_candidates.md +18 -6
  146. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  147. package/templates/planning/optional/references/INDEX.md +3 -3
  148. package/templates/planning/review.md +59 -0
  149. package/templates/planning/task_plan.md +36 -13
  150. package/templates/reference/execution-workflow-standard.md +4 -3
  151. package/templates/reference/pull-request-standard.md +80 -0
  152. package/templates/reference/repo-governance-standard.md +7 -6
  153. package/templates/reference/review-routing-standard.md +6 -0
  154. package/templates/reference/walkthrough-standard.md +2 -1
  155. package/templates/verifier/verifier-output.md +1 -1
  156. package/templates-zh-CN/AGENTS.md.template +20 -16
  157. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  158. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  159. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  160. package/templates-zh-CN/planning/review.md +59 -1
  161. package/templates-zh-CN/planning/task_plan.md +30 -10
  162. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  163. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  164. package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
  165. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  166. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  167. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  168. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  169. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  170. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  171. package/docs-release/assets/dashboard-overview-en.png +0 -0
  172. package/scripts/smoke-dashboard.mjs +0 -92
  173. package/scripts/test-harness.mjs +0 -1395
  174. package/templates/ssot/Feature-SSoT.md +0 -43
  175. package/templates/ssot/Lessons-SSoT.md +0 -44
  176. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  177. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -15,6 +15,7 @@ const state = {
15
15
  renderMode: "rendered",
16
16
  theme: localStorage.getItem("harness.theme") || "system",
17
17
  taskLayout: localStorage.getItem("harness.taskLayout") || "list",
18
+ taskSortOrder: localStorage.getItem("harness.taskSortOrder") === "asc" ? "asc" : "desc",
18
19
  runtime: { mode: "static", csrfToken: "", writableActions: [] },
19
20
  runtimeLoaded: false,
20
21
  runtimePoller: null,
@@ -30,6 +31,8 @@ const taskDocTabs = [
30
31
  ["strategy", "execution_strategy.md"],
31
32
  ["visualMap", "visual_map.md"],
32
33
  ["legacyRoadmap", "visual_roadmap.md"],
34
+ ["lessonCandidates", "lesson_candidates.md"],
35
+ ["longRunningContract", "long-running-task-contract.md"],
33
36
  ["progress", "progress.md"],
34
37
  ["review", "review.md"],
35
38
  ["findings", "findings.md"],
@@ -73,15 +76,26 @@ function shell() {
73
76
  <button data-theme-toggle>${themeLabel()}</button>
74
77
  </div>
75
78
  </header>
79
+ ${runtimeModeBanner()}
76
80
  ${renderRoute()}
77
81
  <div id="drawer-overlay" class="drawer-overlay"></div>
78
82
  <div id="task-drawer" class="task-drawer"></div>
79
83
  </div>`;
80
84
  }
81
85
 
86
+ function runtimeModeBanner() {
87
+ if (window.__HARNESS_WORKBENCH__ === true) return "";
88
+ return `<section class="runtime-banner">
89
+ <strong>${t("staticReadOnly")}</strong>
90
+ <span>${t("staticReadOnlyDetail")}</span>
91
+ <code>harness dev</code>
92
+ </section>`;
93
+ }
94
+
82
95
  function renderRoute() {
83
96
  const route = currentRoute();
84
97
  if (route.name === "task") return taskDetail(route);
98
+ if (route.name === "reviewTask") return reviewWorkspace(route);
85
99
  if (route.name === "review") return reviewQueue();
86
100
  if (route.name === "modules") return modulesView(route.id);
87
101
  if (route.name === "tasks") return taskIndex();
@@ -92,6 +106,7 @@ function currentRoute() {
92
106
  const hash = window.location.hash || "#/";
93
107
  const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean).map(decodeURIComponent);
94
108
  if (parts[0] === "tasks" && parts[1]) return { name: "task", id: parts[1], doc: parts[2] === "docs" ? parts[3] || "" : "" };
109
+ if (parts[0] === "review" && parts[1]) return { name: "reviewTask", id: parts[1] };
95
110
  if (parts[0] === "review") return { name: "review" };
96
111
  if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
97
112
  if (parts[0] === "tasks") return { name: "tasks" };
@@ -99,7 +114,8 @@ function currentRoute() {
99
114
  }
100
115
 
101
116
  function routeLink(hash, text, routeName) {
102
- const active = currentRoute().name === routeName;
117
+ const current = currentRoute().name;
118
+ const active = current === routeName || (routeName === "review" && current === "reviewTask");
103
119
  return `<a class="${active ? "active" : ""}" href="${hash}">${escapeHtml(text)}</a>`;
104
120
  }
105
121
 
@@ -270,24 +286,29 @@ function graphSummary() {
270
286
  }
271
287
 
272
288
  function activeTaskBriefs() {
273
- const tasks = activeTasks().slice(0, 8);
289
+ const tasks = activeTasks();
274
290
  return `<section class="task-briefs">
275
291
  <div class="section-head">
276
292
  <div>
277
293
  <p class="eyebrow">${t("currentWork")}</p>
278
294
  <h2>${t("activeBriefs")}</h2>
279
295
  </div>
280
- <a href="#/tasks">${t("openTaskIndex")}</a>
296
+ <div class="section-actions">
297
+ <span class="subtle">${t("activeBriefCount").replace("{count}", tasks.length).replace("{order}", taskSortLabel())}</span>
298
+ <a href="#/tasks">${t("openTaskIndex")}</a>
299
+ </div>
300
+ </div>
301
+ <div class="brief-scroll">
302
+ <div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
281
303
  </div>
282
- <div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
283
304
  </section>`;
284
305
  }
285
306
 
286
307
  function activeTasks() {
287
308
  const tasks = bundle.status?.tasks || [];
288
309
  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);
310
+ if (active.length > 0) return sortTasksByTime(active);
311
+ return sortTasksByTime(tasks.filter((task) => task.briefSource === "standalone"));
291
312
  }
292
313
 
293
314
  function isActiveTaskState(state) {
@@ -310,6 +331,7 @@ function taskBriefCard(task, { compact = true } = {}) {
310
331
  <p class="brief-teaser">${escapeHtml(summaryText)}</p>
311
332
  </div>
312
333
  <div class="card-actions">
334
+ ${taskCopyButton(task)}
313
335
  <button class="btn-drawer-trigger" data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
314
336
  </div>
315
337
  </article>`;
@@ -349,6 +371,59 @@ function stateToColorVar(state) {
349
371
  return map[state] || "--muted";
350
372
  }
351
373
 
374
+ function taskSortLabel() {
375
+ return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
376
+ }
377
+
378
+ function taskDateKey(task) {
379
+ const source = `${task.shortId || ""} ${task.id || ""}`.trim();
380
+ const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
381
+ if (!match) return null;
382
+ const year = Number(match[1]);
383
+ const month = Number(match[2]);
384
+ const day = Number(match[3] || "1");
385
+ if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
386
+ return Date.UTC(year, month - 1, day);
387
+ }
388
+
389
+ function stableTaskLabel(task) {
390
+ return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
391
+ }
392
+
393
+ function compareTasksByTime(left, right) {
394
+ const leftDate = taskDateKey(left);
395
+ const rightDate = taskDateKey(right);
396
+ if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
397
+ return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
398
+ }
399
+ if (leftDate !== null && rightDate === null) return -1;
400
+ if (leftDate === null && rightDate !== null) return 1;
401
+ return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
402
+ }
403
+
404
+ function sortTasksByTime(tasks) {
405
+ return [...tasks].sort(compareTasksByTime);
406
+ }
407
+
408
+ function taskFolderName(task) {
409
+ const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
410
+ const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
411
+ return task?.shortId || fromPath || fromId || task?.title || "";
412
+ }
413
+
414
+ function taskCopyButton(task, extraClass = "") {
415
+ const folderName = taskFolderName(task);
416
+ return `<button type="button" class="copy-task-name ${extraClass}" data-copy-task-name="${escapeAttr(folderName)}" data-copy-task-folder="${escapeAttr(folderName)}" aria-label="${escapeAttr(t("copyTaskName"))}" title="${escapeAttr(t("copyTaskName"))}">
417
+ ${t("copyTaskNameShort")}
418
+ </button>`;
419
+ }
420
+
421
+ function taskGroupTimeKey(group) {
422
+ const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
423
+ if (!match) return null;
424
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
425
+ }
426
+
352
427
  function taskToolbarCard(filteredCount) {
353
428
  return `<section class="sidebar-card">
354
429
  <h3>${t("filterTitle")}</h3>
@@ -380,6 +455,17 @@ function taskToolbarCard(filteredCount) {
380
455
  </button>
381
456
  </div>
382
457
  </div>
458
+ <div class="select-group">
459
+ <label>${t("sortByTime")}</label>
460
+ <div class="layout-toggle-group sort-toggle-group">
461
+ <button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
462
+ ${t("sortNewest")}
463
+ </button>
464
+ <button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
465
+ ${t("sortOldest")}
466
+ </button>
467
+ </div>
468
+ </div>
383
469
  <div class="search-stats">
384
470
  ${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
385
471
  </div>
@@ -483,11 +569,12 @@ function taskRow(task) {
483
569
  const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
484
570
  const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
485
571
 
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)})">
572
+ return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
487
573
  <div class="row-accent-bar"></div>
488
574
  <div class="row-main">
489
575
  <strong>${escapeHtml(task.title)}</strong>
490
576
  <span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
577
+ ${taskCopyButton(task, "row-copy")}
491
578
  </div>
492
579
  <div class="row-status">${tag(task.state)}</div>
493
580
  <div class="row-progress">
@@ -506,7 +593,7 @@ function taskRow(task) {
506
593
  ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
507
594
  </span>
508
595
  </div>
509
- </a>`;
596
+ </article>`;
510
597
  }
511
598
 
512
599
  function taskIndex() {
@@ -545,7 +632,18 @@ function orderedTaskGroups(groups) {
545
632
  if (group === "unknown") return 3;
546
633
  return 4;
547
634
  };
548
- return Object.entries(groups).sort(([left], [right]) => rank(left) - rank(right) || left.localeCompare(right));
635
+ return Object.entries(groups).sort(([left], [right]) => {
636
+ const rankDiff = rank(left) - rank(right);
637
+ if (rankDiff !== 0) return rankDiff;
638
+ const leftTime = taskGroupTimeKey(left);
639
+ const rightTime = taskGroupTimeKey(right);
640
+ if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
641
+ return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
642
+ }
643
+ if (leftTime !== null && rightTime === null) return -1;
644
+ if (leftTime === null && rightTime !== null) return 1;
645
+ return left.localeCompare(right);
646
+ });
549
647
  }
550
648
 
551
649
  function taskGroups(tasks) {
@@ -570,11 +668,12 @@ function taskGroups(tasks) {
570
668
  }
571
669
 
572
670
  function taskGroup(group, tasks) {
573
- const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
671
+ const orderedTasks = sortTasksByTime(tasks);
672
+ const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
574
673
  const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
575
674
  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;
675
+ const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
676
+ const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
578
677
 
579
678
  const isGrid = state.taskLayout === "grid";
580
679
  const layoutClass = isGrid ? "task-card-grid" : "task-list";
@@ -591,7 +690,7 @@ function taskGroup(group, tasks) {
591
690
  <div class="section-head">
592
691
  <div>
593
692
  <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>
693
+ <p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
595
694
  </div>
596
695
  <div class="group-actions">
597
696
  <div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
@@ -616,10 +715,13 @@ function taskCard(task) {
616
715
  const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
617
716
  const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
618
717
 
619
- return `<a class="task-card" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
718
+ return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
620
719
  <div class="card-header">
621
720
  <span class="card-id">${escapeHtml(task.id)}</span>
622
- ${tag(task.state)}
721
+ <div class="card-header-actions">
722
+ ${taskCopyButton(task, "compact")}
723
+ ${tag(task.state)}
724
+ </div>
623
725
  </div>
624
726
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
625
727
  <div class="card-meta">
@@ -642,7 +744,7 @@ function taskCard(task) {
642
744
  ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
643
745
  </span>
644
746
  </div>
645
- </a>`;
747
+ </article>`;
646
748
  }
647
749
 
648
750
  function taskGroupLabel(group) {
@@ -657,12 +759,12 @@ function taskGroupLabel(group) {
657
759
 
658
760
  function filteredTasks() {
659
761
  const query = state.query.trim().toLowerCase();
660
- return (bundle.status?.tasks || []).filter((task) => {
762
+ return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
661
763
  const stateMatch = state.taskState === "all" || task.state === state.taskState;
662
764
  if (!stateMatch) return false;
663
765
  if (!query) return true;
664
766
  return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
665
- });
767
+ }));
666
768
  }
667
769
 
668
770
  function taskModuleKey(task) {
@@ -680,6 +782,7 @@ function taskDetail(route) {
680
782
  <p class="eyebrow">${t("taskVisibility")}</p>
681
783
  <h2>${escapeHtml(task.title)}</h2>
682
784
  <p>${escapeHtml(task.path)}</p>
785
+ ${taskCopyButton(task, "detail-copy")}
683
786
  </div>
684
787
  <div class="detail-score">${task.completion}%</div>
685
788
  </section>
@@ -690,7 +793,8 @@ function taskDetail(route) {
690
793
  ${taskDocumentLibrary(task, route.doc)}
691
794
  </article>
692
795
  <aside class="detail-side">
693
- ${reviewActionPanel(task)}
796
+ ${reviewActionPanel(task, { mode: "summary" })}
797
+ ${lessonCandidatePanel(task, { context: "detail" })}
694
798
  ${openFindings(task)}
695
799
  ${evidenceList(task)}
696
800
  ${documentTabs(task)}
@@ -713,13 +817,33 @@ function taskStateSummary(task) {
713
817
  <span>${t("reviewStatus")}</span>
714
818
  ${tag(task.reviewStatus || "missing")}
715
819
  </div>
820
+ <div>
821
+ <span>${t("sedimentationStatus")}</span>
822
+ ${tag(task.lessonCandidateStatus || "missing")}
823
+ </div>
716
824
  <div>
717
825
  <span>${t("closeoutStatus")}</span>
718
826
  ${tag(task.closeoutStatus || "missing")}
719
827
  </div>
828
+ <div>
829
+ <span>${t("lifecycleQueues")}</span>
830
+ ${(task.taskQueues || []).map(tag).join("") || tag("active")}
831
+ </div>
832
+ ${taskQueueReasonSummary(task)}
720
833
  </section>`;
721
834
  }
722
835
 
836
+ function taskQueueReasonSummary(task) {
837
+ const reasons = task.queueReasons || [];
838
+ if (!reasons.length) return "";
839
+ return `<div class="task-queue-reasons">
840
+ <span>${t("queueReasons")}</span>
841
+ <div class="review-reasons">
842
+ ${reasons.slice(0, 5).map(reviewReason).join("")}
843
+ </div>
844
+ </div>`;
845
+ }
846
+
723
847
  function phaseTimeline(task) {
724
848
  return `<section class="phase-timeline">
725
849
  <h2>${t("phaseTimeline")}</h2>
@@ -777,7 +901,7 @@ function taskDocumentPriority(task) {
777
901
  const stateName = task?.state || "";
778
902
  const lifecycle = task?.lifecycleState || "";
779
903
  if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
780
- return ["walkthrough", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "legacyRoadmap", "references", "artifacts"];
904
+ return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
781
905
  }
782
906
  if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
783
907
  return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
@@ -831,11 +955,26 @@ function openFindings(task) {
831
955
  </section>`;
832
956
  }
833
957
 
834
- function reviewActionPanel(task) {
835
- if (!canUseWorkbenchAction("review-complete")) return "";
836
- if (!isTaskInReviewStage(task)) return "";
958
+ function reviewActionPanel(task, { mode = "summary" } = {}) {
959
+ if (!isTaskInReviewQueue(task)) return "";
837
960
  const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
838
961
  const confirmed = task.reviewStatus === "confirmed";
962
+ const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
963
+ const candidateStatus = task.lessonCandidateStatus || "missing";
964
+ if (mode !== "workspace") {
965
+ return `<section class="side-panel review-actions">
966
+ <h3>${t("reviewActions")}</h3>
967
+ <p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
968
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
969
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
970
+ </section>`;
971
+ }
972
+ if (!canUseWorkbenchAction("review-complete")) {
973
+ return `<section class="side-panel review-actions">
974
+ <h3>${t("reviewActions")}</h3>
975
+ <p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
976
+ </section>`;
977
+ }
839
978
  if (confirmed) {
840
979
  return `<section class="side-panel review-actions">
841
980
  <h3>${t("reviewActions")}</h3>
@@ -843,26 +982,32 @@ function reviewActionPanel(task) {
843
982
  </section>`;
844
983
  }
845
984
  const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
846
- const disabled = blocking || missingWalkthrough;
847
- const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : t("reviewWorkbenchReady");
985
+ const queueBlocked = !taskCanBeHumanConfirmed(task);
986
+ const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
987
+ const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
848
988
  return `<section class="side-panel review-actions">
849
989
  <h3>${t("reviewActions")}</h3>
850
990
  <p>${escapeHtml(message)}</p>
991
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
851
992
  <label class="review-check">
852
993
  <input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
853
994
  <span>${t("reviewConfirmChecklist")}</span>
854
995
  </label>
996
+ <div class="review-confirm-copy">
997
+ ${taskCopyButton(task, "review-copy-task-name")}
998
+ </div>
855
999
  <input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
856
1000
  <button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
857
1001
  <div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
858
1002
  </section>`;
859
1003
  }
860
1004
 
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);
1005
+ function isTaskInReviewQueue(task) {
1006
+ return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
1007
+ }
1008
+
1009
+ function taskCanBeHumanConfirmed(task) {
1010
+ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
866
1011
  }
867
1012
 
868
1013
  function evidenceList(task) {
@@ -933,10 +1078,16 @@ function moduleCard(module) {
933
1078
  }
934
1079
 
935
1080
  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;
1081
+ ensureReviewQueueState();
1082
+ const tabs = reviewQueueTabs();
1083
+ const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
1084
+ const baseTasks = reviewQueueBaseTasks(activeTab);
1085
+ const reasonOptions = reviewReasonOptions(baseTasks);
1086
+ normalizeReviewReasonFilter(reasonOptions);
1087
+ const tasks = reviewFilteredTasks(baseTasks);
1088
+ const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
1089
+ const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
1090
+ const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
940
1091
  return `<div class="dashboard-grid review-queue-page">
941
1092
  <main class="dashboard-main stack">
942
1093
  <section class="flow-panel">
@@ -946,10 +1097,36 @@ function reviewQueue() {
946
1097
  <h2>${t("reviewQueue")}</h2>
947
1098
  <p class="subtle">${t("reviewQueueSubtitle")}</p>
948
1099
  </div>
949
- <span class="subtle">${ready}/${tasks.length} ${t("reviewReady")}</span>
1100
+ <span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
950
1101
  </div>
951
- <div class="task-card-grid review-queue-grid">
952
- ${tasks.map(reviewQueueCard).join("") || emptyState(t("noReviewTasks"))}
1102
+ <div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
1103
+ ${tabs.map((tab) => reviewQueueTab(tab)).join("")}
1104
+ </div>
1105
+ <div class="review-queue-toolbar">
1106
+ <div class="input-group">
1107
+ <input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
1108
+ </div>
1109
+ <div class="select-group">
1110
+ <label>${t("reasonFilter")}</label>
1111
+ <select data-review-reason-filter aria-label="${t("reasonFilter")}">
1112
+ <option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
1113
+ ${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
1114
+ </select>
1115
+ </div>
1116
+ <div class="select-group">
1117
+ <label>${t("sortBy")}</label>
1118
+ <select data-review-sort aria-label="${t("sortBy")}">
1119
+ ${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
1120
+ </select>
1121
+ </div>
1122
+ </div>
1123
+ <div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
1124
+ <div class="review-queue-list">
1125
+ ${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
1126
+ </div>
1127
+ </div>
1128
+ <div class="review-queue-pager">
1129
+ ${pager("review", page, pageCount)}
953
1130
  </div>
954
1131
  </section>
955
1132
  </main>
@@ -957,52 +1134,239 @@ function reviewQueue() {
957
1134
  <section class="side-panel review-queue-summary">
958
1135
  <h3>${t("reviewQueue")}</h3>
959
1136
  <div class="review-queue-stats">
960
- ${metric(t("reviewReady"), ready)}
961
- ${metric(t("reviewBlockedQueue"), blocked)}
962
- ${metric(t("reviewConfirmedQueue"), confirmed)}
1137
+ ${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
963
1138
  </div>
964
1139
  </section>
965
1140
  <section class="side-panel">
966
- <h3>${t("review")}</h3>
967
- <p>${escapeHtml(t("reviewQueueSubtitle"))}</p>
1141
+ <h3>${escapeHtml(activeTab.label)}</h3>
1142
+ <p>${escapeHtml(activeTab.description)}</p>
1143
+ <dl class="review-queue-contract">
1144
+ <div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
1145
+ <div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
1146
+ </dl>
968
1147
  </section>
969
1148
  </aside>
970
1149
  </div>`;
971
1150
  }
972
1151
 
973
- function reviewQueueTasks() {
974
- return (bundle.status?.tasks || [])
975
- .filter(isTaskInReviewStage)
976
- .sort((left, right) => reviewSortKey(left).localeCompare(reviewSortKey(right)));
1152
+ function ensureReviewQueueState() {
1153
+ if (!state.reviewQueueTab) state.reviewQueueTab = "review";
1154
+ if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
1155
+ if (!state.reviewSort) state.reviewSort = "queue";
1156
+ if (!state.reviewQueuePage) state.reviewQueuePage = 1;
1157
+ }
1158
+
1159
+ function reviewQueueTabs() {
1160
+ return [
1161
+ { id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
1162
+ { id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
1163
+ { id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
1164
+ { id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
1165
+ { id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
1166
+ { id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
1167
+ ];
1168
+ }
1169
+
1170
+ function reviewQueueTab(tab) {
1171
+ const active = tab.id === state.reviewQueueTab;
1172
+ const count = reviewQueueBaseTasks(tab).length;
1173
+ return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
1174
+ <span>${escapeHtml(tab.label)}</span>
1175
+ <strong>${count}</strong>
1176
+ </button>`;
977
1177
  }
978
1178
 
979
- function reviewSortKey(task) {
980
- const rank = task.reviewStatus === "blocked-open-findings" ? "0" : task.reviewStatus === "confirmed" ? "2" : "1";
981
- return `${rank}:${task.id}`;
1179
+ function reviewSortOptions() {
1180
+ return [
1181
+ { id: "queue", label: t("sortQueuePriority") },
1182
+ { id: "newest", label: t("sortNewest") },
1183
+ { id: "oldest", label: t("sortOldest") },
1184
+ { id: "id", label: t("sortTaskId") },
1185
+ ];
982
1186
  }
983
1187
 
984
- function reviewQueueCard(task) {
1188
+ function reviewQueueBaseTasks(tab) {
1189
+ return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
1190
+ }
1191
+
1192
+ function taskMatchesReviewTab(task, tab) {
1193
+ const queues = reviewTaskQueues(task);
1194
+ return (tab.queues || []).some((queue) => queues.includes(queue));
1195
+ }
1196
+
1197
+ function reviewTaskQueues(task) {
1198
+ return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
1199
+ }
1200
+
1201
+ function reviewReasonOptions(tasks) {
1202
+ return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
1203
+ }
1204
+
1205
+ function normalizeReviewReasonFilter(reasonOptions) {
1206
+ const current = state.reviewReasonFilter || "all";
1207
+ if (current === "all") return;
1208
+ if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
1209
+ }
1210
+
1211
+ function reviewFilteredTasks(tasks) {
1212
+ const query = state.query.trim().toLowerCase();
1213
+ const reasonFilter = state.reviewReasonFilter || "all";
1214
+ return [...tasks]
1215
+ .filter((task) => {
1216
+ if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
1217
+ if (!query) return true;
1218
+ return [
1219
+ task.id,
1220
+ task.shortId,
1221
+ task.title,
1222
+ task.module,
1223
+ task.inferredModule,
1224
+ task.state,
1225
+ task.lifecycleState,
1226
+ task.reviewStatus,
1227
+ task.closeoutStatus,
1228
+ ...(task.taskQueues || []),
1229
+ ...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
1230
+ ].some((value) => String(value || "").toLowerCase().includes(query));
1231
+ })
1232
+ .sort(reviewTaskSort);
1233
+ }
1234
+
1235
+ function reviewTaskSort(left, right) {
1236
+ if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
1237
+ if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
1238
+ if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
1239
+ return reviewPriorityRank(left) - reviewPriorityRank(right)
1240
+ || compareTasksByTimeForOrder(left, right, "desc")
1241
+ || stableTaskLabel(left).localeCompare(stableTaskLabel(right));
1242
+ }
1243
+
1244
+ function compareTasksByTimeForOrder(left, right, order) {
1245
+ const previous = state.taskSortOrder;
1246
+ state.taskSortOrder = order;
1247
+ const result = compareTasksByTime(left, right);
1248
+ state.taskSortOrder = previous;
1249
+ return result;
1250
+ }
1251
+
1252
+ function reviewPriorityRank(task) {
1253
+ const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
1254
+ const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
1255
+ const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
1256
+ const queues = reviewTaskQueues(task);
1257
+ const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
1258
+ return Math.min(reasonRank, taskQueueRank);
1259
+ }
1260
+
1261
+ function reviewTruthyCount(tasks, key) {
1262
+ return tasks.filter((task) => task[key] === true).length;
1263
+ }
1264
+
1265
+ function reviewQueueCard(task, tab) {
985
1266
  const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
1267
+ const reasons = task.queueReasons || [];
1268
+ const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
1269
+ const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
1270
+ const displayId = task.shortId || taskFolderName(task) || task.id;
986
1271
  return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
987
1272
  <div class="card-header">
988
- <span class="card-id">${escapeHtml(task.id)}</span>
1273
+ <span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
989
1274
  ${tag(task.reviewStatus || "missing")}
1275
+ ${reviewTaskQueues(task).map(tag).join("")}
990
1276
  </div>
991
1277
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
992
1278
  <div class="card-meta">
993
1279
  <span>${tag(task.lifecycleState || "unknown")}</span>
994
1280
  <span>${tag(task.closeoutStatus || "missing")}</span>
995
1281
  <span>${openMaterial} ${t("openFindings")}</span>
1282
+ <span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
1283
+ <span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
996
1284
  </div>
997
1285
  <p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
1286
+ ${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
1287
+ ${lessonActions}
998
1288
  <div class="review-queue-actions">
1289
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
999
1290
  <a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
1000
1291
  <button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
1292
+ ${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
1001
1293
  </div>
1002
- ${reviewActionPanel(task)}
1003
1294
  </article>`;
1004
1295
  }
1005
1296
 
1297
+ function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
1298
+ const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
1299
+ if (!candidates.length) return "";
1300
+ const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
1301
+ const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
1302
+ const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
1303
+ return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
1304
+ <div class="lesson-candidate-panel-head">
1305
+ <div>
1306
+ <p class="eyebrow">${t("lessonCandidates")}</p>
1307
+ <h3>${t("lessonSedimentationActions")}</h3>
1308
+ </div>
1309
+ <span class="tag">${visibleCandidates.length}/${candidates.length}</span>
1310
+ </div>
1311
+ ${staticNote}
1312
+ <div class="lesson-candidate-actions">
1313
+ ${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
1314
+ </div>
1315
+ ${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
1316
+ </section>`;
1317
+ }
1318
+
1319
+ function lessonCandidateAction(task, candidate) {
1320
+ const followUp = String(candidate.followUpTask || "").trim();
1321
+ const hasFollowUp = followUp && !/^pending$/i.test(followUp);
1322
+ const prompt = lessonSedimentationPrompt(task, candidate);
1323
+ return `<div class="lesson-candidate-action">
1324
+ <div class="lesson-candidate-main">
1325
+ <strong>${escapeHtml(candidate.id)}</strong>
1326
+ <span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
1327
+ <small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
1328
+ </div>
1329
+ <span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
1330
+ <div class="lesson-candidate-command-row">
1331
+ ${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
1332
+ <button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
1333
+ <button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
1334
+ </div>
1335
+ </div>`;
1336
+ }
1337
+
1338
+ function lessonSedimentationPrompt(task, candidate) {
1339
+ return [
1340
+ "You are executing a lesson sedimentation follow-up task.",
1341
+ "",
1342
+ `Source task: ${task.id}`,
1343
+ `Source candidate: ${candidate.id} - ${candidate.title || ""}`,
1344
+ `Candidate scope: ${candidate.scope || "unspecified"}`,
1345
+ `Candidate module key: ${candidate.moduleKey || "n/a"}`,
1346
+ `Detail artifact: ${candidate.detailArtifact || "not provided"}`,
1347
+ `Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
1348
+ `Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
1349
+ `Promotion target: ${candidate.promotionTarget || "unspecified"}`,
1350
+ `Conflict check: ${candidate.conflictCheck || "pending"}`,
1351
+ `Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
1352
+ "",
1353
+ "Instructions:",
1354
+ "1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
1355
+ "2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
1356
+ "3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
1357
+ "4. Check conflicts against existing lessons and standards.",
1358
+ "5. Propose the smallest diff first.",
1359
+ "6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
1360
+ ].join("\n");
1361
+ }
1362
+
1363
+ function reviewReason(reason) {
1364
+ return `<div class="review-reason">
1365
+ <strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
1366
+ <span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
1367
+ </div>`;
1368
+ }
1369
+
1006
1370
  function firstUsefulLine(text) {
1007
1371
  return String(text || "")
1008
1372
  .split(/\n+/)
@@ -1010,6 +1374,57 @@ function firstUsefulLine(text) {
1010
1374
  .filter(Boolean)[0] || "";
1011
1375
  }
1012
1376
 
1377
+ function reviewWorkspace(route) {
1378
+ const task = (bundle.status?.tasks || []).find((item) => item.id === route.id);
1379
+ if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
1380
+ const walkthroughDoc = taskDocument(task, "__walkthrough__");
1381
+ const candidateDoc = taskDocument(task, "lesson_candidates.md");
1382
+ const reviewDoc = taskDocument(task, "review.md");
1383
+ const findingsDoc = taskDocument(task, "findings.md");
1384
+ return `<main class="review-workspace">
1385
+ <nav class="crumbs"><a href="#/review">${t("reviewQueue")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
1386
+ <section class="detail-hero review-hero">
1387
+ <div>
1388
+ <p class="eyebrow">${t("reviewWorkspace")}</p>
1389
+ <h2>${escapeHtml(task.title)}</h2>
1390
+ <p>${escapeHtml(task.path)}</p>
1391
+ </div>
1392
+ <div class="review-hero-tags">
1393
+ ${tag(task.lifecycleState || "unknown")}
1394
+ ${tag(task.reviewStatus || "missing")}
1395
+ ${tag(task.lessonCandidateStatus || "missing")}
1396
+ </div>
1397
+ </section>
1398
+ <section class="review-workspace-grid">
1399
+ <article class="review-workspace-main stack">
1400
+ ${reviewDocPanel("walkthrough", walkthroughDoc, task.walkthroughPath)}
1401
+ ${reviewDocPanel("lessonCandidates", candidateDoc, task.lessonCandidatePath)}
1402
+ ${reviewDocPanel("review", reviewDoc, task.reviewPath)}
1403
+ ${reviewDocPanel("findings", findingsDoc, task.findingsPath)}
1404
+ </article>
1405
+ <aside class="review-workspace-side stack">
1406
+ ${reviewActionPanel(task, { mode: "workspace" })}
1407
+ ${taskStateSummary(task)}
1408
+ ${openFindings(task)}
1409
+ ${evidenceList(task)}
1410
+ </aside>
1411
+ </section>
1412
+ </main>`;
1413
+ }
1414
+
1415
+ function reviewDocPanel(key, doc, fallbackPath = "") {
1416
+ return `<section class="doc-section review-doc-panel">
1417
+ <div class="section-head">
1418
+ <div>
1419
+ <p class="eyebrow">${escapeHtml(fallbackPath || "")}</p>
1420
+ <h2>${t(key)}</h2>
1421
+ </div>
1422
+ ${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
1423
+ </div>
1424
+ <div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
1425
+ </section>`;
1426
+ }
1427
+
1013
1428
  function migrationPanel() {
1014
1429
  const advice = warningQueue();
1015
1430
  const missingBriefs = advice.filter((warning) => warning.type === "missing-brief").length;
@@ -1150,25 +1565,39 @@ function pager(kind, page, pageCount, group = "") {
1150
1565
  }
1151
1566
 
1152
1567
  function lessonPanel() {
1153
- const lessons = (bundle.tables?.tables || [])
1154
- .filter((table) => table.kind === "lessons-ssot")
1155
- .flatMap((table) => table.rows);
1568
+ const lessons = lessonDocuments();
1156
1569
  return `<section class="lesson-panel">
1157
1570
  <div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
1158
1571
  <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>
1572
+ ${lessons.map((lesson) => {
1573
+ return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
1574
+ <strong>${escapeHtml(lesson.id)}</strong>
1575
+ <p>${escapeHtml(lesson.title || lesson.path)}</p>
1166
1576
  </div>`;
1167
1577
  }).join("") || emptyState(t("noLessons"))}
1168
1578
  </div>
1169
1579
  </section>`;
1170
1580
  }
1171
1581
 
1582
+ function lessonDocuments() {
1583
+ return (bundle.documents?.documents || [])
1584
+ .filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
1585
+ .map((doc) => {
1586
+ const id = lessonIdFromDocument(doc);
1587
+ return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
1588
+ })
1589
+ .filter((lesson) => lesson.id)
1590
+ .sort((left, right) => String(right.id).localeCompare(String(left.id)));
1591
+ }
1592
+
1593
+ function lessonIdFromDocument(doc) {
1594
+ const content = doc?.content || "";
1595
+ const path = doc?.path || "";
1596
+ return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
1597
+ || path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
1598
+ || "";
1599
+ }
1600
+
1172
1601
  function healthPanel() {
1173
1602
  const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
1174
1603
  return `<section class="health-panel">
@@ -1272,6 +1701,13 @@ function bind() {
1272
1701
  localStorage.setItem("harness.taskLayout", state.taskLayout);
1273
1702
  app();
1274
1703
  }));
1704
+ document.querySelectorAll("[data-task-sort-order]").forEach((btn) => btn.addEventListener("click", () => {
1705
+ state.taskSortOrder = btn.dataset.taskSortOrder === "asc" ? "asc" : "desc";
1706
+ localStorage.setItem("harness.taskSortOrder", state.taskSortOrder);
1707
+ state.taskPageByGroup = {};
1708
+ state.taskGroupPage = 1;
1709
+ app();
1710
+ }));
1275
1711
  document.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
1276
1712
  state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
1277
1713
  app();
@@ -1286,11 +1722,27 @@ function bind() {
1286
1722
  state.warningPage = 1;
1287
1723
  app();
1288
1724
  }));
1725
+ document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
1726
+ state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
1727
+ state.reviewQueuePage = 1;
1728
+ app();
1729
+ }));
1730
+ document.querySelectorAll("[data-review-reason-filter]").forEach((select) => select.addEventListener("change", () => {
1731
+ state.reviewReasonFilter = select.value || "all";
1732
+ state.reviewQueuePage = 1;
1733
+ app();
1734
+ }));
1735
+ document.querySelectorAll("[data-review-sort]").forEach((select) => select.addEventListener("change", () => {
1736
+ state.reviewSort = select.value || "queue";
1737
+ state.reviewQueuePage = 1;
1738
+ app();
1739
+ }));
1289
1740
  document.querySelectorAll("[data-page-kind]").forEach((button) => button.addEventListener("click", () => {
1290
1741
  const page = Math.max(1, Number(button.dataset.page) || 1);
1291
1742
  if (button.dataset.pageKind === "warning") state.warningPage = page;
1292
1743
  if (button.dataset.pageKind === "task-groups") state.taskGroupPage = page;
1293
1744
  if (button.dataset.pageKind === "task") state.taskPageByGroup[button.dataset.pageGroup || ""] = page;
1745
+ if (button.dataset.pageKind === "review") state.reviewQueuePage = page;
1294
1746
  app();
1295
1747
  }));
1296
1748
  document.querySelectorAll("[data-runway-phase]").forEach((link) => link.addEventListener("click", () => {
@@ -1315,6 +1767,9 @@ function bind() {
1315
1767
  const taskId = el.dataset.openDrawer;
1316
1768
  openDrawer(taskId);
1317
1769
  }));
1770
+ bindCopyTaskNameButtons(document);
1771
+ bindRepairPromptButtons(document);
1772
+ bindLessonSedimentationButtons(document);
1318
1773
  document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
1319
1774
  e.preventDefault();
1320
1775
  const lessonId = el.dataset.openLessonDrawer;
@@ -1404,6 +1859,7 @@ function renderDrawerContent(taskId) {
1404
1859
  <div>
1405
1860
  <h2>${escapeHtml(task.title)}</h2>
1406
1861
  <p style="font-family: var(--font-mono); font-size: 11px; margin: 4px 0 0; color: var(--muted);">${escapeHtml(task.id)}</p>
1862
+ ${taskCopyButton(task, "detail-copy")}
1407
1863
  </div>
1408
1864
  <button class="btn-close" data-close-drawer>×</button>
1409
1865
  </div>
@@ -1416,12 +1872,16 @@ function renderDrawerContent(taskId) {
1416
1872
 
1417
1873
  const body = `
1418
1874
  <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>
1875
+ <div class="drawer-task-summary">
1876
+ <div>
1877
+ <span>${t("statOverall")}</span>
1878
+ <strong>${task.completion}%</strong>
1879
+ </div>
1880
+ <a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger">${t("fullView")}</a>
1422
1881
  </div>
1423
1882
  ${taskStateSummary(task)}
1424
- ${reviewActionPanel(task)}
1883
+ ${reviewActionPanel(task, { mode: "summary" })}
1884
+ ${lessonCandidatePanel(task, { context: "drawer" })}
1425
1885
  ${timeline}
1426
1886
  ${documents}
1427
1887
  ${findings}
@@ -1445,18 +1905,145 @@ function openDrawer(taskId) {
1445
1905
  state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
1446
1906
  openDrawer(taskId);
1447
1907
  }));
1908
+ bindCopyTaskNameButtons(drawer);
1909
+ bindRepairPromptButtons(drawer);
1910
+ bindLessonSedimentationButtons(drawer);
1448
1911
  drawer.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
1449
1912
  }
1450
1913
 
1914
+ function bindCopyTaskNameButtons(root) {
1915
+ root.querySelectorAll("[data-copy-task-name]").forEach((button) => button.addEventListener("click", async (event) => {
1916
+ event.preventDefault();
1917
+ event.stopPropagation();
1918
+ const taskName = button.dataset.copyTaskName || "";
1919
+ const defaultText = t("copyTaskNameShort");
1920
+ try {
1921
+ await copyText(taskName);
1922
+ button.textContent = t("copyTaskNameSuccess");
1923
+ } catch {
1924
+ button.textContent = t("copyTaskNameFailed");
1925
+ }
1926
+ window.setTimeout(() => {
1927
+ button.textContent = defaultText;
1928
+ }, 1400);
1929
+ }));
1930
+ }
1931
+
1932
+ function bindRepairPromptButtons(root) {
1933
+ root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
1934
+ event.preventDefault();
1935
+ event.stopPropagation();
1936
+ const prompt = button.dataset.repairPrompt || "";
1937
+ const defaultText = t("copyRepairPrompt");
1938
+ try {
1939
+ await copyText(prompt);
1940
+ button.textContent = t("copyRepairPromptSuccess");
1941
+ } catch {
1942
+ button.textContent = t("copyTaskNameFailed");
1943
+ }
1944
+ window.setTimeout(() => {
1945
+ button.textContent = defaultText;
1946
+ }, 1400);
1947
+ }));
1948
+ }
1949
+
1950
+ function bindLessonSedimentationButtons(root) {
1951
+ root.querySelectorAll("[data-copy-lesson-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
1952
+ event.preventDefault();
1953
+ event.stopPropagation();
1954
+ const prompt = button.dataset.lessonPrompt || "";
1955
+ const defaultText = t("copyLessonPrompt");
1956
+ try {
1957
+ await copyText(prompt);
1958
+ button.textContent = t("copyRepairPromptSuccess");
1959
+ } catch {
1960
+ button.textContent = t("copyTaskNameFailed");
1961
+ }
1962
+ window.setTimeout(() => {
1963
+ button.textContent = defaultText;
1964
+ }, 1400);
1965
+ }));
1966
+ root.querySelectorAll("[data-create-lesson-sedimentation]").forEach((button) => button.addEventListener("click", async (event) => {
1967
+ event.preventDefault();
1968
+ event.stopPropagation();
1969
+ await createLessonSedimentationFromDashboard(button);
1970
+ }));
1971
+ }
1972
+
1973
+ async function createLessonSedimentationFromDashboard(button) {
1974
+ const taskId = button.dataset.createLessonSedimentation || "";
1975
+ const candidateId = button.dataset.candidateId || "";
1976
+ const result = document.querySelector(`[data-lesson-result="${CSS.escape(`${taskId}:${candidateId}`)}"]`);
1977
+ if (result) result.textContent = t("lessonTaskCreating");
1978
+ button.disabled = true;
1979
+ try {
1980
+ const response = await fetch("/api/tasks/lesson-sedimentation", {
1981
+ method: "POST",
1982
+ headers: {
1983
+ "content-type": "application/json",
1984
+ "x-harness-csrf": state.runtime?.csrfToken || "",
1985
+ },
1986
+ body: JSON.stringify({ taskId, candidateId }),
1987
+ });
1988
+ const payload = await response.json();
1989
+ if (!response.ok) throw payload;
1990
+ if (result) {
1991
+ result.innerHTML = lessonSedimentationSuccess(payload);
1992
+ bindLessonSedimentationButtons(result);
1993
+ result.scrollIntoView({ block: "center", inline: "nearest" });
1994
+ }
1995
+ } catch (error) {
1996
+ button.disabled = false;
1997
+ if (result) result.innerHTML = lessonSedimentationFailure(error);
1998
+ }
1999
+ }
2000
+
2001
+ function lessonSedimentationSuccess(payload) {
2002
+ const followUp = payload?.followUpTask || {};
2003
+ const prompt = payload?.prompt || "";
2004
+ const taskId = followUp.id || "";
2005
+ const openHref = taskId ? `#/tasks/${encodeURIComponent(taskId)}` : "#/review";
2006
+ return `<div class="workbench-action-result success">
2007
+ <strong>${escapeHtml(t("lessonTaskCreated"))}</strong>
2008
+ ${taskId ? `<a href="${openHref}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
2009
+ ${prompt ? `<button data-copy-lesson-prompt="${escapeAttr(taskId || "follow-up")}" data-lesson-prompt="${escapeAttr(prompt)}">${escapeHtml(t("copyLessonPrompt"))}</button>` : ""}
2010
+ </div>`;
2011
+ }
2012
+
2013
+ function lessonSedimentationFailure(error) {
2014
+ const message = error?.error || error?.message || t("lessonTaskCreateFailed");
2015
+ const recovery = Array.isArray(error?.recovery) ? error.recovery : [];
2016
+ const details = error?.details || {};
2017
+ const existingTask = details.followUpTask || details.existingTask || "";
2018
+ return `<div class="workbench-action-result failed">
2019
+ <strong>${escapeHtml(t("lessonTaskCreateFailed"))}</strong>
2020
+ <span>${escapeHtml(message)}</span>
2021
+ ${existingTask ? `<a href="#/tasks/${encodeURIComponent(existingTask)}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
2022
+ ${recovery.length ? `<ul>${recovery.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : ""}
2023
+ </div>`;
2024
+ }
2025
+
2026
+ async function copyText(text) {
2027
+ if (navigator.clipboard?.writeText) {
2028
+ await navigator.clipboard.writeText(text);
2029
+ return;
2030
+ }
2031
+ const textarea = document.createElement("textarea");
2032
+ textarea.value = text;
2033
+ textarea.setAttribute("readonly", "");
2034
+ textarea.style.position = "fixed";
2035
+ textarea.style.left = "-9999px";
2036
+ document.body.appendChild(textarea);
2037
+ textarea.select();
2038
+ const copied = document.execCommand("copy");
2039
+ textarea.remove();
2040
+ if (!copied) throw new Error("copy failed");
2041
+ }
2042
+
1451
2043
  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
- });
2044
+ const lesson = lessonDocuments().find((item) => item.id === lessonId);
1458
2045
 
1459
- if (!row) {
2046
+ if (!lesson) {
1460
2047
  return `<div class="task-drawer-header">
1461
2048
  <h2>${escapeHtml(lessonId)}</h2>
1462
2049
  <button class="btn-close" data-close-drawer>×</button>
@@ -1466,23 +2053,13 @@ function renderLessonDrawerContent(lessonId) {
1466
2053
  </div>`;
1467
2054
  }
1468
2055
 
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
- }
2056
+ const doc = lesson.doc || findDocument(lesson.path);
1480
2057
 
1481
2058
  const header = `
1482
2059
  <div class="task-drawer-header">
1483
2060
  <div>
1484
2061
  <h2>${escapeHtml(lessonId)}</h2>
1485
- <p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(summary)}</p>
2062
+ <p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(lesson.title || lesson.path)}</p>
1486
2063
  </div>
1487
2064
  <button class="btn-close" data-close-drawer>×</button>
1488
2065
  </div>
@@ -1492,16 +2069,10 @@ function renderLessonDrawerContent(lessonId) {
1492
2069
  if (doc && doc.content) {
1493
2070
  markdownBody = `<div class="markdown">${window.HarnessMarkdown.render(doc.content, "rendered")}</div>`;
1494
2071
  } else {
1495
- const rowsHtml = Object.entries(cells)
1496
- .map(([key, val]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(val)}</td></tr>`)
1497
- .join("");
1498
2072
  markdownBody = `
1499
2073
  <div style="margin-bottom: 20px; background: var(--paper-2); padding: 16px; border-radius: 8px; border: 1px dashed var(--line);">
1500
2074
  <p style="margin: 0; font-size: 13px; color: var(--muted);">${t("lessonDocMissing")}</p>
1501
2075
  </div>
1502
- <table class="rendered-table" style="width: 100%;">
1503
- <tbody>${rowsHtml}</tbody>
1504
- </table>
1505
2076
  `;
1506
2077
  }
1507
2078