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
@@ -1,8 +1,14 @@
1
1
  function reviewQueue() {
2
- const tasks = reviewQueueTasks();
3
- const ready = tasks.filter((task) => task.reviewStatus !== "blocked-open-findings" && task.reviewStatus !== "confirmed").length;
4
- const blocked = tasks.filter((task) => task.reviewStatus === "blocked-open-findings").length;
5
- const confirmed = tasks.filter((task) => task.reviewStatus === "confirmed").length;
2
+ ensureReviewQueueState();
3
+ const tabs = reviewQueueTabs();
4
+ const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
5
+ const baseTasks = reviewQueueBaseTasks(activeTab);
6
+ const reasonOptions = reviewReasonOptions(baseTasks);
7
+ normalizeReviewReasonFilter(reasonOptions);
8
+ const tasks = reviewFilteredTasks(baseTasks);
9
+ const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
10
+ const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
11
+ const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
6
12
  return `<div class="dashboard-grid review-queue-page">
7
13
  <main class="dashboard-main stack">
8
14
  <section class="flow-panel">
@@ -12,10 +18,36 @@ function reviewQueue() {
12
18
  <h2>${t("reviewQueue")}</h2>
13
19
  <p class="subtle">${t("reviewQueueSubtitle")}</p>
14
20
  </div>
15
- <span class="subtle">${ready}/${tasks.length} ${t("reviewReady")}</span>
21
+ <span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
16
22
  </div>
17
- <div class="task-card-grid review-queue-grid">
18
- ${tasks.map(reviewQueueCard).join("") || emptyState(t("noReviewTasks"))}
23
+ <div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
24
+ ${tabs.map((tab) => reviewQueueTab(tab)).join("")}
25
+ </div>
26
+ <div class="review-queue-toolbar">
27
+ <div class="input-group">
28
+ <input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
29
+ </div>
30
+ <div class="select-group">
31
+ <label>${t("reasonFilter")}</label>
32
+ <select data-review-reason-filter aria-label="${t("reasonFilter")}">
33
+ <option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
34
+ ${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
35
+ </select>
36
+ </div>
37
+ <div class="select-group">
38
+ <label>${t("sortBy")}</label>
39
+ <select data-review-sort aria-label="${t("sortBy")}">
40
+ ${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
41
+ </select>
42
+ </div>
43
+ </div>
44
+ <div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
45
+ <div class="review-queue-list">
46
+ ${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
47
+ </div>
48
+ </div>
49
+ <div class="review-queue-pager">
50
+ ${pager("review", page, pageCount)}
19
51
  </div>
20
52
  </section>
21
53
  </main>
@@ -23,52 +55,239 @@ function reviewQueue() {
23
55
  <section class="side-panel review-queue-summary">
24
56
  <h3>${t("reviewQueue")}</h3>
25
57
  <div class="review-queue-stats">
26
- ${metric(t("reviewReady"), ready)}
27
- ${metric(t("reviewBlockedQueue"), blocked)}
28
- ${metric(t("reviewConfirmedQueue"), confirmed)}
58
+ ${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
29
59
  </div>
30
60
  </section>
31
61
  <section class="side-panel">
32
- <h3>${t("review")}</h3>
33
- <p>${escapeHtml(t("reviewQueueSubtitle"))}</p>
62
+ <h3>${escapeHtml(activeTab.label)}</h3>
63
+ <p>${escapeHtml(activeTab.description)}</p>
64
+ <dl class="review-queue-contract">
65
+ <div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
66
+ <div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
67
+ </dl>
34
68
  </section>
35
69
  </aside>
36
70
  </div>`;
37
71
  }
38
72
 
39
- function reviewQueueTasks() {
40
- return (bundle.status?.tasks || [])
41
- .filter(isTaskInReviewStage)
42
- .sort((left, right) => reviewSortKey(left).localeCompare(reviewSortKey(right)));
73
+ function ensureReviewQueueState() {
74
+ if (!state.reviewQueueTab) state.reviewQueueTab = "review";
75
+ if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
76
+ if (!state.reviewSort) state.reviewSort = "queue";
77
+ if (!state.reviewQueuePage) state.reviewQueuePage = 1;
78
+ }
79
+
80
+ function reviewQueueTabs() {
81
+ return [
82
+ { id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
83
+ { id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
84
+ { id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
85
+ { id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
86
+ { id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
87
+ { id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
88
+ ];
89
+ }
90
+
91
+ function reviewQueueTab(tab) {
92
+ const active = tab.id === state.reviewQueueTab;
93
+ const count = reviewQueueBaseTasks(tab).length;
94
+ return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
95
+ <span>${escapeHtml(tab.label)}</span>
96
+ <strong>${count}</strong>
97
+ </button>`;
98
+ }
99
+
100
+ function reviewSortOptions() {
101
+ return [
102
+ { id: "queue", label: t("sortQueuePriority") },
103
+ { id: "newest", label: t("sortNewest") },
104
+ { id: "oldest", label: t("sortOldest") },
105
+ { id: "id", label: t("sortTaskId") },
106
+ ];
107
+ }
108
+
109
+ function reviewQueueBaseTasks(tab) {
110
+ return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
111
+ }
112
+
113
+ function taskMatchesReviewTab(task, tab) {
114
+ const queues = reviewTaskQueues(task);
115
+ return (tab.queues || []).some((queue) => queues.includes(queue));
116
+ }
117
+
118
+ function reviewTaskQueues(task) {
119
+ return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
120
+ }
121
+
122
+ function reviewReasonOptions(tasks) {
123
+ return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
124
+ }
125
+
126
+ function normalizeReviewReasonFilter(reasonOptions) {
127
+ const current = state.reviewReasonFilter || "all";
128
+ if (current === "all") return;
129
+ if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
130
+ }
131
+
132
+ function reviewFilteredTasks(tasks) {
133
+ const query = state.query.trim().toLowerCase();
134
+ const reasonFilter = state.reviewReasonFilter || "all";
135
+ return [...tasks]
136
+ .filter((task) => {
137
+ if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
138
+ if (!query) return true;
139
+ return [
140
+ task.id,
141
+ task.shortId,
142
+ task.title,
143
+ task.module,
144
+ task.inferredModule,
145
+ task.state,
146
+ task.lifecycleState,
147
+ task.reviewStatus,
148
+ task.closeoutStatus,
149
+ ...(task.taskQueues || []),
150
+ ...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
151
+ ].some((value) => String(value || "").toLowerCase().includes(query));
152
+ })
153
+ .sort(reviewTaskSort);
154
+ }
155
+
156
+ function reviewTaskSort(left, right) {
157
+ if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
158
+ if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
159
+ if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
160
+ return reviewPriorityRank(left) - reviewPriorityRank(right)
161
+ || compareTasksByTimeForOrder(left, right, "desc")
162
+ || stableTaskLabel(left).localeCompare(stableTaskLabel(right));
163
+ }
164
+
165
+ function compareTasksByTimeForOrder(left, right, order) {
166
+ const previous = state.taskSortOrder;
167
+ state.taskSortOrder = order;
168
+ const result = compareTasksByTime(left, right);
169
+ state.taskSortOrder = previous;
170
+ return result;
171
+ }
172
+
173
+ function reviewPriorityRank(task) {
174
+ const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
175
+ const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
176
+ const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
177
+ const queues = reviewTaskQueues(task);
178
+ const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
179
+ return Math.min(reasonRank, taskQueueRank);
43
180
  }
44
181
 
45
- function reviewSortKey(task) {
46
- const rank = task.reviewStatus === "blocked-open-findings" ? "0" : task.reviewStatus === "confirmed" ? "2" : "1";
47
- return `${rank}:${task.id}`;
182
+ function reviewTruthyCount(tasks, key) {
183
+ return tasks.filter((task) => task[key] === true).length;
48
184
  }
49
185
 
50
- function reviewQueueCard(task) {
186
+ function reviewQueueCard(task, tab) {
51
187
  const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
188
+ const reasons = task.queueReasons || [];
189
+ const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
190
+ const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
191
+ const displayId = task.shortId || taskFolderName(task) || task.id;
52
192
  return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
53
193
  <div class="card-header">
54
- <span class="card-id">${escapeHtml(task.id)}</span>
194
+ <span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
55
195
  ${tag(task.reviewStatus || "missing")}
196
+ ${reviewTaskQueues(task).map(tag).join("")}
56
197
  </div>
57
198
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
58
199
  <div class="card-meta">
59
200
  <span>${tag(task.lifecycleState || "unknown")}</span>
60
201
  <span>${tag(task.closeoutStatus || "missing")}</span>
61
202
  <span>${openMaterial} ${t("openFindings")}</span>
203
+ <span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
204
+ <span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
62
205
  </div>
63
206
  <p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
207
+ ${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
208
+ ${lessonActions}
64
209
  <div class="review-queue-actions">
65
210
  <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
66
211
  <a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
67
212
  <button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
213
+ ${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
68
214
  </div>
69
215
  </article>`;
70
216
  }
71
217
 
218
+ function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
219
+ const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
220
+ if (!candidates.length) return "";
221
+ const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
222
+ const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
223
+ const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
224
+ return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
225
+ <div class="lesson-candidate-panel-head">
226
+ <div>
227
+ <p class="eyebrow">${t("lessonCandidates")}</p>
228
+ <h3>${t("lessonSedimentationActions")}</h3>
229
+ </div>
230
+ <span class="tag">${visibleCandidates.length}/${candidates.length}</span>
231
+ </div>
232
+ ${staticNote}
233
+ <div class="lesson-candidate-actions">
234
+ ${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
235
+ </div>
236
+ ${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
237
+ </section>`;
238
+ }
239
+
240
+ function lessonCandidateAction(task, candidate) {
241
+ const followUp = String(candidate.followUpTask || "").trim();
242
+ const hasFollowUp = followUp && !/^pending$/i.test(followUp);
243
+ const prompt = lessonSedimentationPrompt(task, candidate);
244
+ return `<div class="lesson-candidate-action">
245
+ <div class="lesson-candidate-main">
246
+ <strong>${escapeHtml(candidate.id)}</strong>
247
+ <span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
248
+ <small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
249
+ </div>
250
+ <span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
251
+ <div class="lesson-candidate-command-row">
252
+ ${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
253
+ <button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
254
+ <button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
255
+ </div>
256
+ </div>`;
257
+ }
258
+
259
+ function lessonSedimentationPrompt(task, candidate) {
260
+ return [
261
+ "You are executing a lesson sedimentation follow-up task.",
262
+ "",
263
+ `Source task: ${task.id}`,
264
+ `Source candidate: ${candidate.id} - ${candidate.title || ""}`,
265
+ `Candidate scope: ${candidate.scope || "unspecified"}`,
266
+ `Candidate module key: ${candidate.moduleKey || "n/a"}`,
267
+ `Detail artifact: ${candidate.detailArtifact || "not provided"}`,
268
+ `Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
269
+ `Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
270
+ `Promotion target: ${candidate.promotionTarget || "unspecified"}`,
271
+ `Conflict check: ${candidate.conflictCheck || "pending"}`,
272
+ `Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
273
+ "",
274
+ "Instructions:",
275
+ "1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
276
+ "2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
277
+ "3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
278
+ "4. Check conflicts against existing lessons and standards.",
279
+ "5. Propose the smallest diff first.",
280
+ "6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
281
+ ].join("\n");
282
+ }
283
+
284
+ function reviewReason(reason) {
285
+ return `<div class="review-reason">
286
+ <strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
287
+ <span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
288
+ </div>`;
289
+ }
290
+
72
291
  function firstUsefulLine(text) {
73
292
  return String(text || "")
74
293
  .split(/\n+/)
@@ -123,6 +342,6 @@ function reviewDocPanel(key, doc, fallbackPath = "") {
123
342
  </div>
124
343
  ${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
125
344
  </div>
126
- <div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div>
345
+ <div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
127
346
  </section>`;
128
347
  }
@@ -138,25 +138,39 @@ function pager(kind, page, pageCount, group = "") {
138
138
  }
139
139
 
140
140
  function lessonPanel() {
141
- const lessons = (bundle.tables?.tables || [])
142
- .filter((table) => table.kind === "lessons-ssot")
143
- .flatMap((table) => table.rows);
141
+ const lessons = lessonDocuments();
144
142
  return `<section class="lesson-panel">
145
143
  <div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
146
144
  <div class="lesson-list" style="padding-top: 10px;">
147
- ${lessons.map((row) => {
148
- const cells = row.cells || {};
149
- const lessonId = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
150
- const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
151
- return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lessonId)}">
152
- <strong>${escapeHtml(lessonId)}</strong>
153
- <p>${escapeHtml(summary)}</p>
145
+ ${lessons.map((lesson) => {
146
+ return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
147
+ <strong>${escapeHtml(lesson.id)}</strong>
148
+ <p>${escapeHtml(lesson.title || lesson.path)}</p>
154
149
  </div>`;
155
150
  }).join("") || emptyState(t("noLessons"))}
156
151
  </div>
157
152
  </section>`;
158
153
  }
159
154
 
155
+ function lessonDocuments() {
156
+ return (bundle.documents?.documents || [])
157
+ .filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
158
+ .map((doc) => {
159
+ const id = lessonIdFromDocument(doc);
160
+ return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
161
+ })
162
+ .filter((lesson) => lesson.id)
163
+ .sort((left, right) => String(right.id).localeCompare(String(left.id)));
164
+ }
165
+
166
+ function lessonIdFromDocument(doc) {
167
+ const content = doc?.content || "";
168
+ const path = doc?.path || "";
169
+ return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
170
+ || path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
171
+ || "";
172
+ }
173
+
160
174
  function healthPanel() {
161
175
  const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
162
176
  return `<section class="health-panel">
@@ -28,6 +28,13 @@ function bind() {
28
28
  localStorage.setItem("harness.taskLayout", state.taskLayout);
29
29
  app();
30
30
  }));
31
+ document.querySelectorAll("[data-task-sort-order]").forEach((btn) => btn.addEventListener("click", () => {
32
+ state.taskSortOrder = btn.dataset.taskSortOrder === "asc" ? "asc" : "desc";
33
+ localStorage.setItem("harness.taskSortOrder", state.taskSortOrder);
34
+ state.taskPageByGroup = {};
35
+ state.taskGroupPage = 1;
36
+ app();
37
+ }));
31
38
  document.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
32
39
  state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
33
40
  app();
@@ -42,11 +49,27 @@ function bind() {
42
49
  state.warningPage = 1;
43
50
  app();
44
51
  }));
52
+ document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
53
+ state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
54
+ state.reviewQueuePage = 1;
55
+ app();
56
+ }));
57
+ document.querySelectorAll("[data-review-reason-filter]").forEach((select) => select.addEventListener("change", () => {
58
+ state.reviewReasonFilter = select.value || "all";
59
+ state.reviewQueuePage = 1;
60
+ app();
61
+ }));
62
+ document.querySelectorAll("[data-review-sort]").forEach((select) => select.addEventListener("change", () => {
63
+ state.reviewSort = select.value || "queue";
64
+ state.reviewQueuePage = 1;
65
+ app();
66
+ }));
45
67
  document.querySelectorAll("[data-page-kind]").forEach((button) => button.addEventListener("click", () => {
46
68
  const page = Math.max(1, Number(button.dataset.page) || 1);
47
69
  if (button.dataset.pageKind === "warning") state.warningPage = page;
48
70
  if (button.dataset.pageKind === "task-groups") state.taskGroupPage = page;
49
71
  if (button.dataset.pageKind === "task") state.taskPageByGroup[button.dataset.pageGroup || ""] = page;
72
+ if (button.dataset.pageKind === "review") state.reviewQueuePage = page;
50
73
  app();
51
74
  }));
52
75
  document.querySelectorAll("[data-runway-phase]").forEach((link) => link.addEventListener("click", () => {
@@ -71,6 +94,9 @@ function bind() {
71
94
  const taskId = el.dataset.openDrawer;
72
95
  openDrawer(taskId);
73
96
  }));
97
+ bindCopyTaskNameButtons(document);
98
+ bindRepairPromptButtons(document);
99
+ bindLessonSedimentationButtons(document);
74
100
  document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
75
101
  e.preventDefault();
76
102
  const lessonId = el.dataset.openLessonDrawer;
@@ -160,6 +186,7 @@ function renderDrawerContent(taskId) {
160
186
  <div>
161
187
  <h2>${escapeHtml(task.title)}</h2>
162
188
  <p style="font-family: var(--font-mono); font-size: 11px; margin: 4px 0 0; color: var(--muted);">${escapeHtml(task.id)}</p>
189
+ ${taskCopyButton(task, "detail-copy")}
163
190
  </div>
164
191
  <button class="btn-close" data-close-drawer>×</button>
165
192
  </div>
@@ -172,12 +199,16 @@ function renderDrawerContent(taskId) {
172
199
 
173
200
  const body = `
174
201
  <div class="task-drawer-body stack">
175
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: var(--paper-2); padding: 12px 16px; border-radius: 8px;">
176
- <div style="font-size: 24px; font-weight: 800; color: var(--accent);">${task.completion}%</div>
177
- <a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger" style="text-decoration: none;">${t("fullView")}</a>
202
+ <div class="drawer-task-summary">
203
+ <div>
204
+ <span>${t("statOverall")}</span>
205
+ <strong>${task.completion}%</strong>
206
+ </div>
207
+ <a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger">${t("fullView")}</a>
178
208
  </div>
179
209
  ${taskStateSummary(task)}
180
210
  ${reviewActionPanel(task, { mode: "summary" })}
211
+ ${lessonCandidatePanel(task, { context: "drawer" })}
181
212
  ${timeline}
182
213
  ${documents}
183
214
  ${findings}
@@ -201,18 +232,145 @@ function openDrawer(taskId) {
201
232
  state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
202
233
  openDrawer(taskId);
203
234
  }));
235
+ bindCopyTaskNameButtons(drawer);
236
+ bindRepairPromptButtons(drawer);
237
+ bindLessonSedimentationButtons(drawer);
204
238
  drawer.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
205
239
  }
206
240
 
241
+ function bindCopyTaskNameButtons(root) {
242
+ root.querySelectorAll("[data-copy-task-name]").forEach((button) => button.addEventListener("click", async (event) => {
243
+ event.preventDefault();
244
+ event.stopPropagation();
245
+ const taskName = button.dataset.copyTaskName || "";
246
+ const defaultText = t("copyTaskNameShort");
247
+ try {
248
+ await copyText(taskName);
249
+ button.textContent = t("copyTaskNameSuccess");
250
+ } catch {
251
+ button.textContent = t("copyTaskNameFailed");
252
+ }
253
+ window.setTimeout(() => {
254
+ button.textContent = defaultText;
255
+ }, 1400);
256
+ }));
257
+ }
258
+
259
+ function bindRepairPromptButtons(root) {
260
+ root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
261
+ event.preventDefault();
262
+ event.stopPropagation();
263
+ const prompt = button.dataset.repairPrompt || "";
264
+ const defaultText = t("copyRepairPrompt");
265
+ try {
266
+ await copyText(prompt);
267
+ button.textContent = t("copyRepairPromptSuccess");
268
+ } catch {
269
+ button.textContent = t("copyTaskNameFailed");
270
+ }
271
+ window.setTimeout(() => {
272
+ button.textContent = defaultText;
273
+ }, 1400);
274
+ }));
275
+ }
276
+
277
+ function bindLessonSedimentationButtons(root) {
278
+ root.querySelectorAll("[data-copy-lesson-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
279
+ event.preventDefault();
280
+ event.stopPropagation();
281
+ const prompt = button.dataset.lessonPrompt || "";
282
+ const defaultText = t("copyLessonPrompt");
283
+ try {
284
+ await copyText(prompt);
285
+ button.textContent = t("copyRepairPromptSuccess");
286
+ } catch {
287
+ button.textContent = t("copyTaskNameFailed");
288
+ }
289
+ window.setTimeout(() => {
290
+ button.textContent = defaultText;
291
+ }, 1400);
292
+ }));
293
+ root.querySelectorAll("[data-create-lesson-sedimentation]").forEach((button) => button.addEventListener("click", async (event) => {
294
+ event.preventDefault();
295
+ event.stopPropagation();
296
+ await createLessonSedimentationFromDashboard(button);
297
+ }));
298
+ }
299
+
300
+ async function createLessonSedimentationFromDashboard(button) {
301
+ const taskId = button.dataset.createLessonSedimentation || "";
302
+ const candidateId = button.dataset.candidateId || "";
303
+ const result = document.querySelector(`[data-lesson-result="${CSS.escape(`${taskId}:${candidateId}`)}"]`);
304
+ if (result) result.textContent = t("lessonTaskCreating");
305
+ button.disabled = true;
306
+ try {
307
+ const response = await fetch("/api/tasks/lesson-sedimentation", {
308
+ method: "POST",
309
+ headers: {
310
+ "content-type": "application/json",
311
+ "x-harness-csrf": state.runtime?.csrfToken || "",
312
+ },
313
+ body: JSON.stringify({ taskId, candidateId }),
314
+ });
315
+ const payload = await response.json();
316
+ if (!response.ok) throw payload;
317
+ if (result) {
318
+ result.innerHTML = lessonSedimentationSuccess(payload);
319
+ bindLessonSedimentationButtons(result);
320
+ result.scrollIntoView({ block: "center", inline: "nearest" });
321
+ }
322
+ } catch (error) {
323
+ button.disabled = false;
324
+ if (result) result.innerHTML = lessonSedimentationFailure(error);
325
+ }
326
+ }
327
+
328
+ function lessonSedimentationSuccess(payload) {
329
+ const followUp = payload?.followUpTask || {};
330
+ const prompt = payload?.prompt || "";
331
+ const taskId = followUp.id || "";
332
+ const openHref = taskId ? `#/tasks/${encodeURIComponent(taskId)}` : "#/review";
333
+ return `<div class="workbench-action-result success">
334
+ <strong>${escapeHtml(t("lessonTaskCreated"))}</strong>
335
+ ${taskId ? `<a href="${openHref}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
336
+ ${prompt ? `<button data-copy-lesson-prompt="${escapeAttr(taskId || "follow-up")}" data-lesson-prompt="${escapeAttr(prompt)}">${escapeHtml(t("copyLessonPrompt"))}</button>` : ""}
337
+ </div>`;
338
+ }
339
+
340
+ function lessonSedimentationFailure(error) {
341
+ const message = error?.error || error?.message || t("lessonTaskCreateFailed");
342
+ const recovery = Array.isArray(error?.recovery) ? error.recovery : [];
343
+ const details = error?.details || {};
344
+ const existingTask = details.followUpTask || details.existingTask || "";
345
+ return `<div class="workbench-action-result failed">
346
+ <strong>${escapeHtml(t("lessonTaskCreateFailed"))}</strong>
347
+ <span>${escapeHtml(message)}</span>
348
+ ${existingTask ? `<a href="#/tasks/${encodeURIComponent(existingTask)}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
349
+ ${recovery.length ? `<ul>${recovery.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : ""}
350
+ </div>`;
351
+ }
352
+
353
+ async function copyText(text) {
354
+ if (navigator.clipboard?.writeText) {
355
+ await navigator.clipboard.writeText(text);
356
+ return;
357
+ }
358
+ const textarea = document.createElement("textarea");
359
+ textarea.value = text;
360
+ textarea.setAttribute("readonly", "");
361
+ textarea.style.position = "fixed";
362
+ textarea.style.left = "-9999px";
363
+ document.body.appendChild(textarea);
364
+ textarea.select();
365
+ const copied = document.execCommand("copy");
366
+ textarea.remove();
367
+ if (!copied) throw new Error("copy failed");
368
+ }
369
+
207
370
  function renderLessonDrawerContent(lessonId) {
208
- const lessonTable = (bundle.tables?.tables || []).find((table) => table.kind === "lessons-ssot");
209
- const row = (lessonTable?.rows || []).find((r) => {
210
- const cells = r.cells || {};
211
- const id = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
212
- return id === lessonId;
213
- });
214
-
215
- if (!row) {
371
+ const lesson = lessonDocuments().find((item) => item.id === lessonId);
372
+
373
+ if (!lesson) {
216
374
  return `<div class="task-drawer-header">
217
375
  <h2>${escapeHtml(lessonId)}</h2>
218
376
  <button class="btn-close" data-close-drawer>×</button>
@@ -222,23 +380,13 @@ function renderLessonDrawerContent(lessonId) {
222
380
  </div>`;
223
381
  }
224
382
 
225
- const cells = row.cells || {};
226
- const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
227
- const docPath = cells["\u8be6\u60c5\u6587\u6863"] || cells.Document || cells.document || "";
228
-
229
- let doc = null;
230
- if (docPath) {
231
- doc = findDocument(docPath);
232
- }
233
- if (!doc) {
234
- doc = (bundle.documents?.documents || []).find((d) => d.path.includes(lessonId) || d.path.endsWith(`${lessonId}.md`));
235
- }
383
+ const doc = lesson.doc || findDocument(lesson.path);
236
384
 
237
385
  const header = `
238
386
  <div class="task-drawer-header">
239
387
  <div>
240
388
  <h2>${escapeHtml(lessonId)}</h2>
241
- <p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(summary)}</p>
389
+ <p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(lesson.title || lesson.path)}</p>
242
390
  </div>
243
391
  <button class="btn-close" data-close-drawer>×</button>
244
392
  </div>
@@ -248,16 +396,10 @@ function renderLessonDrawerContent(lessonId) {
248
396
  if (doc && doc.content) {
249
397
  markdownBody = `<div class="markdown">${window.HarnessMarkdown.render(doc.content, "rendered")}</div>`;
250
398
  } else {
251
- const rowsHtml = Object.entries(cells)
252
- .map(([key, val]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(val)}</td></tr>`)
253
- .join("");
254
399
  markdownBody = `
255
400
  <div style="margin-bottom: 20px; background: var(--paper-2); padding: 16px; border-radius: 8px; border: 1px dashed var(--line);">
256
401
  <p style="margin: 0; font-size: 13px; color: var(--muted);">${t("lessonDocMissing")}</p>
257
402
  </div>
258
- <table class="rendered-table" style="width: 100%;">
259
- <tbody>${rowsHtml}</tbody>
260
- </table>
261
403
  `;
262
404
  }
263
405