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
@@ -8,6 +8,59 @@ function stateToColorVar(state) {
8
8
  return map[state] || "--muted";
9
9
  }
10
10
 
11
+ function taskSortLabel() {
12
+ return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
13
+ }
14
+
15
+ function taskDateKey(task) {
16
+ const source = `${task.shortId || ""} ${task.id || ""}`.trim();
17
+ const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
18
+ if (!match) return null;
19
+ const year = Number(match[1]);
20
+ const month = Number(match[2]);
21
+ const day = Number(match[3] || "1");
22
+ if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
23
+ return Date.UTC(year, month - 1, day);
24
+ }
25
+
26
+ function stableTaskLabel(task) {
27
+ return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
28
+ }
29
+
30
+ function compareTasksByTime(left, right) {
31
+ const leftDate = taskDateKey(left);
32
+ const rightDate = taskDateKey(right);
33
+ if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
34
+ return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
35
+ }
36
+ if (leftDate !== null && rightDate === null) return -1;
37
+ if (leftDate === null && rightDate !== null) return 1;
38
+ return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
39
+ }
40
+
41
+ function sortTasksByTime(tasks) {
42
+ return [...tasks].sort(compareTasksByTime);
43
+ }
44
+
45
+ function taskFolderName(task) {
46
+ const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
47
+ const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
48
+ return task?.shortId || fromPath || fromId || task?.title || "";
49
+ }
50
+
51
+ function taskCopyButton(task, extraClass = "") {
52
+ const folderName = taskFolderName(task);
53
+ 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"))}">
54
+ ${t("copyTaskNameShort")}
55
+ </button>`;
56
+ }
57
+
58
+ function taskGroupTimeKey(group) {
59
+ const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
60
+ if (!match) return null;
61
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
62
+ }
63
+
11
64
  function taskToolbarCard(filteredCount) {
12
65
  return `<section class="sidebar-card">
13
66
  <h3>${t("filterTitle")}</h3>
@@ -39,6 +92,17 @@ function taskToolbarCard(filteredCount) {
39
92
  </button>
40
93
  </div>
41
94
  </div>
95
+ <div class="select-group">
96
+ <label>${t("sortByTime")}</label>
97
+ <div class="layout-toggle-group sort-toggle-group">
98
+ <button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
99
+ ${t("sortNewest")}
100
+ </button>
101
+ <button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
102
+ ${t("sortOldest")}
103
+ </button>
104
+ </div>
105
+ </div>
42
106
  <div class="search-stats">
43
107
  ${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
44
108
  </div>
@@ -142,11 +206,12 @@ function taskRow(task) {
142
206
  const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
143
207
  const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
144
208
 
145
- return `<a class="task-row-card" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
209
+ return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
146
210
  <div class="row-accent-bar"></div>
147
211
  <div class="row-main">
148
212
  <strong>${escapeHtml(task.title)}</strong>
149
213
  <span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
214
+ ${taskCopyButton(task, "row-copy")}
150
215
  </div>
151
216
  <div class="row-status">${tag(task.state)}</div>
152
217
  <div class="row-progress">
@@ -165,7 +230,7 @@ function taskRow(task) {
165
230
  ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
166
231
  </span>
167
232
  </div>
168
- </a>`;
233
+ </article>`;
169
234
  }
170
235
 
171
236
  function taskIndex() {
@@ -204,7 +269,18 @@ function orderedTaskGroups(groups) {
204
269
  if (group === "unknown") return 3;
205
270
  return 4;
206
271
  };
207
- return Object.entries(groups).sort(([left], [right]) => rank(left) - rank(right) || left.localeCompare(right));
272
+ return Object.entries(groups).sort(([left], [right]) => {
273
+ const rankDiff = rank(left) - rank(right);
274
+ if (rankDiff !== 0) return rankDiff;
275
+ const leftTime = taskGroupTimeKey(left);
276
+ const rightTime = taskGroupTimeKey(right);
277
+ if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
278
+ return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
279
+ }
280
+ if (leftTime !== null && rightTime === null) return -1;
281
+ if (leftTime === null && rightTime !== null) return 1;
282
+ return left.localeCompare(right);
283
+ });
208
284
  }
209
285
 
210
286
  function taskGroups(tasks) {
@@ -229,11 +305,12 @@ function taskGroups(tasks) {
229
305
  }
230
306
 
231
307
  function taskGroup(group, tasks) {
232
- const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
308
+ const orderedTasks = sortTasksByTime(tasks);
309
+ const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
233
310
  const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
234
311
  const start = (page - 1) * taskPageSize;
235
- const visibleTasks = tasks.slice(start, start + taskPageSize);
236
- const avgCompletion = tasks.length ? clampCompletion(tasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / tasks.length) : 0;
312
+ const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
313
+ const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
237
314
 
238
315
  const isGrid = state.taskLayout === "grid";
239
316
  const layoutClass = isGrid ? "task-card-grid" : "task-list";
@@ -250,7 +327,7 @@ function taskGroup(group, tasks) {
250
327
  <div class="section-head">
251
328
  <div>
252
329
  <h2>${taskGroupLabel(group)}</h2>
253
- <p class="subtle">${t("showing")} ${Math.min(start + 1, tasks.length)}-${Math.min(start + visibleTasks.length, tasks.length)} / ${tasks.length}</p>
330
+ <p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
254
331
  </div>
255
332
  <div class="group-actions">
256
333
  <div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
@@ -275,10 +352,13 @@ function taskCard(task) {
275
352
  const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
276
353
  const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
277
354
 
278
- return `<a class="task-card" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
355
+ return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
279
356
  <div class="card-header">
280
357
  <span class="card-id">${escapeHtml(task.id)}</span>
281
- ${tag(task.state)}
358
+ <div class="card-header-actions">
359
+ ${taskCopyButton(task, "compact")}
360
+ ${tag(task.state)}
361
+ </div>
282
362
  </div>
283
363
  <h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
284
364
  <div class="card-meta">
@@ -301,7 +381,7 @@ function taskCard(task) {
301
381
  ${mapReady ? t("badgeMap") : t("badgeMapMissing")}
302
382
  </span>
303
383
  </div>
304
- </a>`;
384
+ </article>`;
305
385
  }
306
386
 
307
387
  function taskGroupLabel(group) {
@@ -316,248 +396,14 @@ function taskGroupLabel(group) {
316
396
 
317
397
  function filteredTasks() {
318
398
  const query = state.query.trim().toLowerCase();
319
- return (bundle.status?.tasks || []).filter((task) => {
399
+ return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
320
400
  const stateMatch = state.taskState === "all" || task.state === state.taskState;
321
401
  if (!stateMatch) return false;
322
402
  if (!query) return true;
323
403
  return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
324
- });
404
+ }));
325
405
  }
326
406
 
327
407
  function taskModuleKey(task) {
328
408
  return task.module || task.inferredModule || "legacy-unclassified";
329
409
  }
330
-
331
- function taskDetail(route) {
332
- const taskId = route.id;
333
- const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
334
- if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
335
- return `<main class="task-detail">
336
- <nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
337
- <section class="detail-hero">
338
- <div>
339
- <p class="eyebrow">${t("taskVisibility")}</p>
340
- <h2>${escapeHtml(task.title)}</h2>
341
- <p>${escapeHtml(task.path)}</p>
342
- </div>
343
- <div class="detail-score">${task.completion}%</div>
344
- </section>
345
- ${taskStateSummary(task)}
346
- ${phaseTimeline(task)}
347
- <section class="detail-grid">
348
- <article class="detail-main">
349
- ${taskDocumentLibrary(task, route.doc)}
350
- </article>
351
- <aside class="detail-side">
352
- ${reviewActionPanel(task, { mode: "summary" })}
353
- ${migrationSnapshotPanel(task)}
354
- ${openFindings(task)}
355
- ${evidenceList(task)}
356
- ${documentTabs(task)}
357
- </aside>
358
- </section>
359
- </main>`;
360
- }
361
-
362
- function taskStateSummary(task) {
363
- return `<section class="task-state-summary">
364
- <div>
365
- <span>${t("legacyState")}</span>
366
- ${tag(task.state)}
367
- </div>
368
- <div>
369
- <span>${t("lifecycleState")}</span>
370
- ${tag(task.lifecycleState || "unknown")}
371
- </div>
372
- <div>
373
- <span>${t("reviewStatus")}</span>
374
- ${tag(task.reviewStatus || "missing")}
375
- </div>
376
- <div>
377
- <span>${t("closeoutStatus")}</span>
378
- ${tag(task.closeoutStatus || "missing")}
379
- </div>
380
- </section>`;
381
- }
382
-
383
- function phaseTimeline(task) {
384
- return `<section class="phase-timeline">
385
- <h2>${t("phaseTimeline")}</h2>
386
- ${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
387
- <strong>${escapeHtml(phase.id)}</strong>
388
- <span>${phase.completion}%</span>
389
- <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
390
- ${progressBar(phase.completion)}
391
- </div>`).join("") || emptyState(t("noPhaseData"))}
392
- </section>`;
393
- }
394
-
395
- function taskDocSection(task, fileName, title, required) {
396
- const doc = taskDocument(task, fileName);
397
- if (!doc && !required) return "";
398
- return `<section class="doc-section">
399
- <div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
400
- <div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
401
- </section>`;
402
- }
403
-
404
- function taskDocumentLibrary(task, selectedTab) {
405
- const docs = orderedTaskDocuments(task);
406
- if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
407
- const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
408
- return `<section class="doc-library">
409
- <div class="section-head">
410
- <div>
411
- <p class="eyebrow">${t("taskDocuments")}</p>
412
- <h2>${escapeHtml(t("sourceDocuments"))}</h2>
413
- </div>
414
- <button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
415
- </div>
416
- <div class="doc-accordion-list">
417
- ${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
418
- </div>
419
- </section>`;
420
- }
421
-
422
- function orderedTaskDocuments(task) {
423
- const docs = taskDocTabs
424
- .map(([key, file]) => {
425
- const doc = taskDocument(task, file);
426
- if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
427
- if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
428
- return null;
429
- })
430
- .filter(Boolean);
431
- const priority = taskDocumentPriority(task);
432
- const rank = new Map(priority.map((key, index) => [key, index]));
433
- return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
434
- }
435
-
436
- function taskDocumentPriority(task) {
437
- const stateName = task?.state || "";
438
- const lifecycle = task?.lifecycleState || "";
439
- if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
440
- return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
441
- }
442
- if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
443
- return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
444
- }
445
- if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
446
- return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
447
- }
448
- return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
449
- }
450
-
451
- function defaultTaskDocumentKey(task, docs) {
452
- const priority = taskDocumentPriority(task);
453
- return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
454
- }
455
-
456
- function documentAccordion(item, open) {
457
- return `<details class="doc-accordion" ${open ? "open" : ""}>
458
- <summary>
459
- <span>${escapeHtml(item.title)}</span>
460
- <small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
461
- </summary>
462
- <div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
463
- </details>`;
464
- }
465
-
466
- function documentTabs(task) {
467
- const docs = orderedTaskDocuments(task);
468
- return `<section class="side-panel">
469
- <h3>${t("sourceDocuments")}</h3>
470
- ${docs.map((doc) => `<a href="#/tasks/${encodeURIComponent(task.id)}/docs/${encodeURIComponent(doc.key)}" title="${escapeAttr(doc.path)}">${escapeHtml(doc.title)}</a>`).join("") || `<p>${t("noDocuments")}</p>`}
471
- </section>`;
472
- }
473
-
474
- function selectedSourceDocument(task, tab) {
475
- if (!tab) return "";
476
- const match = taskDocTabs.find(([key]) => key === tab);
477
- if (!match) return "";
478
- const doc = taskDocument(task, match[1]);
479
- if (!doc) return "";
480
- return `<section class="doc-section selected-source">
481
- <div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
482
- <div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
483
- </section>`;
484
- }
485
-
486
- function openFindings(task) {
487
- const risks = task.risks || [];
488
- return `<section class="side-panel">
489
- <h3>${t("openFindings")}</h3>
490
- ${risks.map((risk) => `<div class="finding ${risk.open || risk.blocksRelease ? "open" : ""}"><strong>${escapeHtml(risk.severity)}</strong><span>${escapeHtml(risk.summary)}</span></div>`).join("") || `<p>${t("noOpenFindings")}</p>`}
491
- </section>`;
492
- }
493
-
494
- function migrationSnapshotPanel(task) {
495
- const snapshot = task.migrationSnapshot;
496
- if (!snapshot) return "";
497
- return `<section class="side-panel">
498
- <h3>${t("migrationSnapshot")}</h3>
499
- <p>${escapeHtml(t("taskPreset"))}: ${tag(task.taskPreset || "none")}</p>
500
- <p>${escapeHtml(t("targetLevel"))}: ${tag(snapshot.targetLevel || "unknown")}</p>
501
- <p>${escapeHtml(t("achievedLevel"))}: ${tag(snapshot.achievedLevel || "unknown")}</p>
502
- <p>${escapeHtml(t("strictDeferred"))}: ${tag(snapshot.strictDeferred ? "yes" : "no")}</p>
503
- <p>${escapeHtml(t("evidenceBundle"))}: <code>${escapeHtml(snapshot.evidenceBundle || "missing")}</code></p>
504
- </section>`;
505
- }
506
-
507
- function reviewActionPanel(task, { mode = "summary" } = {}) {
508
- if (!isTaskInReviewStage(task)) return "";
509
- const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
510
- const confirmed = task.reviewStatus === "confirmed";
511
- const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
512
- const candidateStatus = task.lessonCandidateStatus || "missing";
513
- if (mode !== "workspace") {
514
- return `<section class="side-panel review-actions">
515
- <h3>${t("reviewActions")}</h3>
516
- <p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
517
- <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
518
- <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
519
- </section>`;
520
- }
521
- if (!canUseWorkbenchAction("review-complete")) {
522
- return `<section class="side-panel review-actions">
523
- <h3>${t("reviewActions")}</h3>
524
- <p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
525
- </section>`;
526
- }
527
- if (confirmed) {
528
- return `<section class="side-panel review-actions">
529
- <h3>${t("reviewActions")}</h3>
530
- <p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
531
- </section>`;
532
- }
533
- const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
534
- const disabled = blocking || missingWalkthrough || candidateBlocked;
535
- const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : t("reviewWorkbenchReady");
536
- return `<section class="side-panel review-actions">
537
- <h3>${t("reviewActions")}</h3>
538
- <p>${escapeHtml(message)}</p>
539
- <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
540
- <label class="review-check">
541
- <input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
542
- <span>${t("reviewConfirmChecklist")}</span>
543
- </label>
544
- <input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
545
- <button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
546
- <div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
547
- </section>`;
548
- }
549
-
550
- function isTaskInReviewStage(task) {
551
- const state = task?.state || "";
552
- const lifecycle = task?.lifecycleState || "";
553
- if (["not_started", "planned", "in_progress"].includes(state)) return false;
554
- return state === "review" || ["in_review", "review-blocked"].includes(lifecycle);
555
- }
556
-
557
- function evidenceList(task) {
558
- const evidence = task.evidence || [];
559
- return `<section class="side-panel">
560
- <h3>${t("evidence")}</h3>
561
- ${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
562
- </section>`;
563
- }
@@ -0,0 +1,246 @@
1
+ function taskDetail(route) {
2
+ const taskId = route.id;
3
+ const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
4
+ if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
5
+ return `<main class="task-detail">
6
+ <nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
7
+ <section class="detail-hero">
8
+ <div>
9
+ <p class="eyebrow">${t("taskVisibility")}</p>
10
+ <h2>${escapeHtml(task.title)}</h2>
11
+ <p>${escapeHtml(task.path)}</p>
12
+ ${taskCopyButton(task, "detail-copy")}
13
+ </div>
14
+ <div class="detail-score">${task.completion}%</div>
15
+ </section>
16
+ ${taskStateSummary(task)}
17
+ ${phaseTimeline(task)}
18
+ <section class="detail-grid">
19
+ <article class="detail-main">
20
+ ${taskDocumentLibrary(task, route.doc)}
21
+ </article>
22
+ <aside class="detail-side">
23
+ ${reviewActionPanel(task, { mode: "summary" })}
24
+ ${lessonCandidatePanel(task, { context: "detail" })}
25
+ ${openFindings(task)}
26
+ ${evidenceList(task)}
27
+ ${documentTabs(task)}
28
+ </aside>
29
+ </section>
30
+ </main>`;
31
+ }
32
+
33
+ function taskStateSummary(task) {
34
+ return `<section class="task-state-summary">
35
+ <div>
36
+ <span>${t("legacyState")}</span>
37
+ ${tag(task.state)}
38
+ </div>
39
+ <div>
40
+ <span>${t("lifecycleState")}</span>
41
+ ${tag(task.lifecycleState || "unknown")}
42
+ </div>
43
+ <div>
44
+ <span>${t("reviewStatus")}</span>
45
+ ${tag(task.reviewStatus || "missing")}
46
+ </div>
47
+ <div>
48
+ <span>${t("sedimentationStatus")}</span>
49
+ ${tag(task.lessonCandidateStatus || "missing")}
50
+ </div>
51
+ <div>
52
+ <span>${t("closeoutStatus")}</span>
53
+ ${tag(task.closeoutStatus || "missing")}
54
+ </div>
55
+ <div>
56
+ <span>${t("lifecycleQueues")}</span>
57
+ ${(task.taskQueues || []).map(tag).join("") || tag("active")}
58
+ </div>
59
+ ${taskQueueReasonSummary(task)}
60
+ </section>`;
61
+ }
62
+
63
+ function taskQueueReasonSummary(task) {
64
+ const reasons = task.queueReasons || [];
65
+ if (!reasons.length) return "";
66
+ return `<div class="task-queue-reasons">
67
+ <span>${t("queueReasons")}</span>
68
+ <div class="review-reasons">
69
+ ${reasons.slice(0, 5).map(reviewReason).join("")}
70
+ </div>
71
+ </div>`;
72
+ }
73
+
74
+ function phaseTimeline(task) {
75
+ return `<section class="phase-timeline">
76
+ <h2>${t("phaseTimeline")}</h2>
77
+ ${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
78
+ <strong>${escapeHtml(phase.id)}</strong>
79
+ <span>${phase.completion}%</span>
80
+ <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
81
+ ${progressBar(phase.completion)}
82
+ </div>`).join("") || emptyState(t("noPhaseData"))}
83
+ </section>`;
84
+ }
85
+
86
+ function taskDocSection(task, fileName, title, required) {
87
+ const doc = taskDocument(task, fileName);
88
+ if (!doc && !required) return "";
89
+ return `<section class="doc-section">
90
+ <div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
91
+ <div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
92
+ </section>`;
93
+ }
94
+
95
+ function taskDocumentLibrary(task, selectedTab) {
96
+ const docs = orderedTaskDocuments(task);
97
+ if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
98
+ const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
99
+ return `<section class="doc-library">
100
+ <div class="section-head">
101
+ <div>
102
+ <p class="eyebrow">${t("taskDocuments")}</p>
103
+ <h2>${escapeHtml(t("sourceDocuments"))}</h2>
104
+ </div>
105
+ <button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
106
+ </div>
107
+ <div class="doc-accordion-list">
108
+ ${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
109
+ </div>
110
+ </section>`;
111
+ }
112
+
113
+ function orderedTaskDocuments(task) {
114
+ const docs = taskDocTabs
115
+ .map(([key, file]) => {
116
+ const doc = taskDocument(task, file);
117
+ if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
118
+ if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
119
+ return null;
120
+ })
121
+ .filter(Boolean);
122
+ const priority = taskDocumentPriority(task);
123
+ const rank = new Map(priority.map((key, index) => [key, index]));
124
+ return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
125
+ }
126
+
127
+ function taskDocumentPriority(task) {
128
+ const stateName = task?.state || "";
129
+ const lifecycle = task?.lifecycleState || "";
130
+ if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
131
+ return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
132
+ }
133
+ if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
134
+ return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
135
+ }
136
+ if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
137
+ return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
138
+ }
139
+ return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
140
+ }
141
+
142
+ function defaultTaskDocumentKey(task, docs) {
143
+ const priority = taskDocumentPriority(task);
144
+ return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
145
+ }
146
+
147
+ function documentAccordion(item, open) {
148
+ return `<details class="doc-accordion" ${open ? "open" : ""}>
149
+ <summary>
150
+ <span>${escapeHtml(item.title)}</span>
151
+ <small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
152
+ </summary>
153
+ <div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
154
+ </details>`;
155
+ }
156
+
157
+ function documentTabs(task) {
158
+ const docs = orderedTaskDocuments(task);
159
+ return `<section class="side-panel">
160
+ <h3>${t("sourceDocuments")}</h3>
161
+ ${docs.map((doc) => `<a href="#/tasks/${encodeURIComponent(task.id)}/docs/${encodeURIComponent(doc.key)}" title="${escapeAttr(doc.path)}">${escapeHtml(doc.title)}</a>`).join("") || `<p>${t("noDocuments")}</p>`}
162
+ </section>`;
163
+ }
164
+
165
+ function selectedSourceDocument(task, tab) {
166
+ if (!tab) return "";
167
+ const match = taskDocTabs.find(([key]) => key === tab);
168
+ if (!match) return "";
169
+ const doc = taskDocument(task, match[1]);
170
+ if (!doc) return "";
171
+ return `<section class="doc-section selected-source">
172
+ <div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
173
+ <div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
174
+ </section>`;
175
+ }
176
+
177
+ function openFindings(task) {
178
+ const risks = task.risks || [];
179
+ return `<section class="side-panel">
180
+ <h3>${t("openFindings")}</h3>
181
+ ${risks.map((risk) => `<div class="finding ${risk.open || risk.blocksRelease ? "open" : ""}"><strong>${escapeHtml(risk.severity)}</strong><span>${escapeHtml(risk.summary)}</span></div>`).join("") || `<p>${t("noOpenFindings")}</p>`}
182
+ </section>`;
183
+ }
184
+
185
+ function reviewActionPanel(task, { mode = "summary" } = {}) {
186
+ if (!isTaskInReviewQueue(task)) return "";
187
+ const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
188
+ const confirmed = task.reviewStatus === "confirmed";
189
+ const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
190
+ const candidateStatus = task.lessonCandidateStatus || "missing";
191
+ if (mode !== "workspace") {
192
+ return `<section class="side-panel review-actions">
193
+ <h3>${t("reviewActions")}</h3>
194
+ <p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
195
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
196
+ <a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
197
+ </section>`;
198
+ }
199
+ if (!canUseWorkbenchAction("review-complete")) {
200
+ return `<section class="side-panel review-actions">
201
+ <h3>${t("reviewActions")}</h3>
202
+ <p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
203
+ </section>`;
204
+ }
205
+ if (confirmed) {
206
+ return `<section class="side-panel review-actions">
207
+ <h3>${t("reviewActions")}</h3>
208
+ <p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
209
+ </section>`;
210
+ }
211
+ const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
212
+ const queueBlocked = !taskCanBeHumanConfirmed(task);
213
+ const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
214
+ const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
215
+ return `<section class="side-panel review-actions">
216
+ <h3>${t("reviewActions")}</h3>
217
+ <p>${escapeHtml(message)}</p>
218
+ <p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
219
+ <label class="review-check">
220
+ <input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
221
+ <span>${t("reviewConfirmChecklist")}</span>
222
+ </label>
223
+ <div class="review-confirm-copy">
224
+ ${taskCopyButton(task, "review-copy-task-name")}
225
+ </div>
226
+ <input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
227
+ <button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
228
+ <div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
229
+ </section>`;
230
+ }
231
+
232
+ function isTaskInReviewQueue(task) {
233
+ return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
234
+ }
235
+
236
+ function taskCanBeHumanConfirmed(task) {
237
+ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
238
+ }
239
+
240
+ function evidenceList(task) {
241
+ const evidence = task.evidence || [];
242
+ return `<section class="side-panel">
243
+ <h3>${t("evidence")}</h3>
244
+ ${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
245
+ </section>`;
246
+ }