coding-agent-harness 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/SKILL.md +423 -0
  5. package/docs-release/README.md +30 -0
  6. package/docs-release/architecture/overview.md +52 -0
  7. package/docs-release/guides/agent-installation.md +139 -0
  8. package/examples/minimal-project/.harness-capabilities.json +8 -0
  9. package/examples/minimal-project/AGENTS.md +4 -0
  10. package/examples/minimal-project/CLAUDE.md +3 -0
  11. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/execution_strategy.md +10 -0
  12. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +11 -0
  13. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/review.md +27 -0
  14. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +14 -0
  15. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/visual_roadmap.md +11 -0
  16. package/examples/minimal-project/docs/Harness-Ledger.md +6 -0
  17. package/package.json +34 -0
  18. package/references/adversarial-review-standard.md +173 -0
  19. package/references/agents-md-pattern.md +140 -0
  20. package/references/cadence-ledger.md +55 -0
  21. package/references/ci-cd-standard.md +90 -0
  22. package/references/delivery-operating-model-standard.md +145 -0
  23. package/references/docs-directory-standard.md +125 -0
  24. package/references/harness-ledger.md +148 -0
  25. package/references/lessons-governance.md +157 -0
  26. package/references/long-running-task-standard.md +209 -0
  27. package/references/module-parallel-standard.md +292 -0
  28. package/references/planning-loop.md +192 -0
  29. package/references/project-onboarding-audit.md +167 -0
  30. package/references/regression-system.md +89 -0
  31. package/references/repo-governance-standard.md +131 -0
  32. package/references/review-routing-standard.md +103 -0
  33. package/references/ssot-governance.md +111 -0
  34. package/references/walkthrough-closeout.md +135 -0
  35. package/references/worktree-parallel.md +184 -0
  36. package/scripts/check-harness.mjs +728 -0
  37. package/scripts/harness.mjs +201 -0
  38. package/scripts/lib/dashboard-writer.mjs +95 -0
  39. package/scripts/lib/harness-core.mjs +1318 -0
  40. package/scripts/smoke-dashboard.mjs +70 -0
  41. package/scripts/test-harness.mjs +482 -0
  42. package/templates/AGENTS.md.template +82 -0
  43. package/templates/CLAUDE.md.template +12 -0
  44. package/templates/dashboard/assets/app.css +399 -0
  45. package/templates/dashboard/assets/app.js +435 -0
  46. package/templates/dashboard/assets/i18n.js +47 -0
  47. package/templates/dashboard/assets/markdown-reader.js +116 -0
  48. package/templates/dashboard/assets/mermaid-renderer.js +59 -0
  49. package/templates/dashboard/index.html +18 -0
  50. package/templates/ledger/Harness-Ledger.md +39 -0
  51. package/templates/lessons/lesson-arch-process-change.md +47 -0
  52. package/templates/lessons/lesson-new-doc.md +50 -0
  53. package/templates/lessons/lesson-ref-change.md +45 -0
  54. package/templates/planning/execution_strategy.md +40 -0
  55. package/templates/planning/findings.md +24 -0
  56. package/templates/planning/long-running-task-contract.md +69 -0
  57. package/templates/planning/module_plan.md +36 -0
  58. package/templates/planning/module_session_prompt.md +39 -0
  59. package/templates/planning/optional/artifacts/INDEX.md +12 -0
  60. package/templates/planning/optional/references/INDEX.md +13 -0
  61. package/templates/planning/optional/slices/_slice-template/brief.md +27 -0
  62. package/templates/planning/optional/slices/_slice-template/evidence.md +9 -0
  63. package/templates/planning/optional/slices/_slice-template/review.md +31 -0
  64. package/templates/planning/progress.md +33 -0
  65. package/templates/planning/review.md +48 -0
  66. package/templates/planning/task_plan.md +86 -0
  67. package/templates/planning/visual_roadmap.md +28 -0
  68. package/templates/reference/adversarial-review-standard.md +28 -0
  69. package/templates/reference/ci-cd-standard.md +28 -0
  70. package/templates/reference/delivery-operating-model-standard.md +28 -0
  71. package/templates/reference/docs-library-standard.md +28 -0
  72. package/templates/reference/engineering-standard.md +29 -0
  73. package/templates/reference/execution-workflow-standard.md +29 -0
  74. package/templates/reference/harness-ledger-standard.md +26 -0
  75. package/templates/reference/long-running-task-standard.md +28 -0
  76. package/templates/reference/regression-ssot-governance.md +28 -0
  77. package/templates/reference/repo-governance-standard.md +29 -0
  78. package/templates/reference/review-routing-standard.md +29 -0
  79. package/templates/reference/testing-standard.md +28 -0
  80. package/templates/reference/walkthrough-standard.md +28 -0
  81. package/templates/reference/worktree-standard.md +28 -0
  82. package/templates/regression/Cadence-Ledger.md +41 -0
  83. package/templates/ssot/Delivery-SSoT.md +43 -0
  84. package/templates/ssot/Feature-SSoT.md +43 -0
  85. package/templates/ssot/Lessons-SSoT.md +44 -0
  86. package/templates/ssot/Module-Registry.md +43 -0
  87. package/templates/ssot/Regression-SSoT.md +51 -0
  88. package/templates/verifier/verifier-output.md +43 -0
  89. package/templates/walkthrough/Closeout-SSoT.md +43 -0
  90. package/templates/walkthrough/walkthrough-template.md +63 -0
  91. package/templates-zh-CN/AGENTS.md.template +92 -0
  92. package/templates-zh-CN/CLAUDE.md.template +12 -0
  93. package/templates-zh-CN/dashboard/assets/app.css +399 -0
  94. package/templates-zh-CN/dashboard/assets/app.js +435 -0
  95. package/templates-zh-CN/dashboard/assets/i18n.js +47 -0
  96. package/templates-zh-CN/dashboard/assets/markdown-reader.js +116 -0
  97. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +59 -0
  98. package/templates-zh-CN/dashboard/index.html +18 -0
  99. package/templates-zh-CN/ledger/Harness-Ledger.md +50 -0
  100. package/templates-zh-CN/lessons/lesson-arch-process-change.md +47 -0
  101. package/templates-zh-CN/lessons/lesson-new-doc.md +49 -0
  102. package/templates-zh-CN/lessons/lesson-ref-change.md +59 -0
  103. package/templates-zh-CN/planning/execution_strategy.md +37 -0
  104. package/templates-zh-CN/planning/findings.md +24 -0
  105. package/templates-zh-CN/planning/long-running-task-contract.md +118 -0
  106. package/templates-zh-CN/planning/module_plan.md +43 -0
  107. package/templates-zh-CN/planning/module_session_prompt.md +70 -0
  108. package/templates-zh-CN/planning/optional/artifacts/INDEX.md +13 -0
  109. package/templates-zh-CN/planning/optional/references/INDEX.md +13 -0
  110. package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +35 -0
  111. package/templates-zh-CN/planning/optional/slices/_slice-template/evidence.md +12 -0
  112. package/templates-zh-CN/planning/optional/slices/_slice-template/review.md +37 -0
  113. package/templates-zh-CN/planning/progress.md +29 -0
  114. package/templates-zh-CN/planning/review.md +69 -0
  115. package/templates-zh-CN/planning/task_plan.md +116 -0
  116. package/templates-zh-CN/planning/visual_roadmap.md +24 -0
  117. package/templates-zh-CN/reference/adversarial-review-standard.md +89 -0
  118. package/templates-zh-CN/reference/ci-cd-standard.md +72 -0
  119. package/templates-zh-CN/reference/delivery-operating-model-standard.md +79 -0
  120. package/templates-zh-CN/reference/docs-library-standard.md +59 -0
  121. package/templates-zh-CN/reference/engineering-standard.md +80 -0
  122. package/templates-zh-CN/reference/execution-workflow-standard.md +81 -0
  123. package/templates-zh-CN/reference/harness-ledger-standard.md +91 -0
  124. package/templates-zh-CN/reference/long-running-task-standard.md +156 -0
  125. package/templates-zh-CN/reference/regression-ssot-governance.md +82 -0
  126. package/templates-zh-CN/reference/repo-governance-standard.md +84 -0
  127. package/templates-zh-CN/reference/review-routing-standard.md +82 -0
  128. package/templates-zh-CN/reference/testing-standard.md +72 -0
  129. package/templates-zh-CN/reference/walkthrough-standard.md +83 -0
  130. package/templates-zh-CN/reference/worktree-standard.md +116 -0
  131. package/templates-zh-CN/regression/Cadence-Ledger.md +48 -0
  132. package/templates-zh-CN/ssot/Delivery-SSoT.md +60 -0
  133. package/templates-zh-CN/ssot/Feature-SSoT.md +49 -0
  134. package/templates-zh-CN/ssot/Lessons-SSoT.md +49 -0
  135. package/templates-zh-CN/ssot/Module-Registry.md +48 -0
  136. package/templates-zh-CN/ssot/Regression-SSoT.md +51 -0
  137. package/templates-zh-CN/verifier/verifier-output.md +38 -0
  138. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +42 -0
  139. package/templates-zh-CN/walkthrough/walkthrough-template.md +62 -0
@@ -0,0 +1,435 @@
1
+ const bundle = window.__HARNESS_DASHBOARD__ || {};
2
+ const state = {
3
+ page: "overview",
4
+ lang: "en",
5
+ theme: localStorage.getItem("harness.theme") || "system",
6
+ density: localStorage.getItem("harness.density") || "comfortable",
7
+ selected: null,
8
+ tab: "plan",
9
+ renderMode: "rendered",
10
+ };
11
+
12
+ const pageKeys = ["overview", "ledger", "tasks", "modules", "evidence", "lessons", "adoption", "settings"];
13
+ const taskDocTabs = [
14
+ ["plan", "task_plan.md"],
15
+ ["strategy", "execution_strategy.md"],
16
+ ["roadmap", "visual_roadmap.md"],
17
+ ["progress", "progress.md"],
18
+ ["review", "review.md"],
19
+ ["findings", "findings.md"],
20
+ ["references", "references/INDEX.md"],
21
+ ["artifacts", "artifacts/INDEX.md"],
22
+ ];
23
+
24
+ function t(key) {
25
+ return (window.HarnessI18n?.en || {})[key] || key;
26
+ }
27
+
28
+ function app() {
29
+ const systemTheme = window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
30
+ document.documentElement.dataset.theme = state.theme === "system" ? systemTheme : state.theme;
31
+ document.documentElement.dataset.density = state.density;
32
+ document.documentElement.lang = "en";
33
+ const root = document.getElementById("app");
34
+ root.innerHTML = `
35
+ <div class="layout">
36
+ <aside class="sidebar">
37
+ <div class="brand">
38
+ <strong>${escapeHtml(bundle.status?.project?.name || "Harness")}</strong>
39
+ <span>${escapeHtml(bundle.status?.project?.root || "TARGET:.")}</span>
40
+ </div>
41
+ <nav class="nav">${pageKeys.map((page) => navButton(page)).join("")}</nav>
42
+ </aside>
43
+ <main class="main">
44
+ ${topbar()}
45
+ ${renderPage()}
46
+ </main>
47
+ </div>`;
48
+ bind();
49
+ }
50
+
51
+ function navButton(page) {
52
+ return `<button data-page="${page}" class="${state.page === page ? "active" : ""}">${t(page)}</button>`;
53
+ }
54
+
55
+ function topbar() {
56
+ return `<div class="topbar">
57
+ <div>
58
+ <p class="eyebrow">Coding Agent Harness Dashboard</p>
59
+ <h1>${escapeHtml(pageTitle())}</h1>
60
+ </div>
61
+ <div class="controls">
62
+ <div class="control">
63
+ <button data-theme="light" class="${state.theme === "light" ? "active" : ""}">${t("light")}</button>
64
+ <button data-theme="dark" class="${state.theme === "dark" ? "active" : ""}">${t("dark")}</button>
65
+ <button data-theme="system" class="${state.theme === "system" ? "active" : ""}">${t("system")}</button>
66
+ </div>
67
+ <div class="control">
68
+ <button data-density="compact" class="${state.density === "compact" ? "active" : ""}">${t("compact")}</button>
69
+ <button data-density="comfortable" class="${state.density === "comfortable" ? "active" : ""}">${t("comfortable")}</button>
70
+ </div>
71
+ </div>
72
+ </div>`;
73
+ }
74
+
75
+ function pageTitle() {
76
+ const project = bundle.status?.project?.name || "project";
77
+ if (state.page === "overview") return `${project} project cockpit`;
78
+ return t(state.page);
79
+ }
80
+
81
+ function renderPage() {
82
+ if (state.page === "overview") return overview();
83
+ if (state.page === "ledger") return withDrawer(ledgerTable());
84
+ if (state.page === "tasks") return withDrawer(taskTable());
85
+ if (state.page === "modules") return withDrawer(moduleTable());
86
+ if (state.page === "evidence") return withDrawer(evidenceTable());
87
+ if (state.page === "lessons") return withDrawer(lessonsTable());
88
+ if (state.page === "adoption") return adoption();
89
+ return settings();
90
+ }
91
+
92
+ function overview() {
93
+ const status = bundle.status || {};
94
+ const evidence = evidenceHealth(status.tasks || []);
95
+ const blockers = status.checkState?.failures || 0;
96
+ const warnings = status.checkState?.warnings || 0;
97
+ return `<section class="status-grid">
98
+ <div class="readiness panel">
99
+ <span class="state ${status.checkState?.status || "warn"}">${t("readiness")}: ${label(status.checkState?.status || "unknown")}</span>
100
+ <h2>${nextActionText()}</h2>
101
+ <p class="muted">Blockers, evidence gaps, and adoption advice are separated.</p>
102
+ </div>
103
+ ${metric(t("activeTasks"), status.tasks?.length || 0)}
104
+ ${metric(t("blockers"), blockers)}
105
+ ${metric(t("warnings"), warnings)}
106
+ </section>
107
+ <section class="page-grid">
108
+ <div>
109
+ ${overviewTasks()}
110
+ ${ledgerSummary()}
111
+ ${riskPanel()}
112
+ </div>
113
+ ${drawer()}
114
+ </section>`;
115
+ }
116
+
117
+ function metric(label, value) {
118
+ return `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`;
119
+ }
120
+
121
+ function nextActionText() {
122
+ const failures = bundle.status?.checkState?.failures || 0;
123
+ if (failures > 0) return "Resolve release blockers first";
124
+ const advice = bundle.adoption?.warnings?.length || 0;
125
+ if (advice > 0) return "Proceed, with adoption advice to address";
126
+ return "No blockers in this snapshot";
127
+ }
128
+
129
+ function overviewTasks() {
130
+ const rows = (bundle.status?.tasks || []).slice(0, 6);
131
+ return tablePanel(t("tasks"), [t("task"), t("state"), t("completion"), t("roadmapSource")], rows.map((task) => [
132
+ clickable(task.title, "task", task.id),
133
+ tag(task.state, label(task.state)),
134
+ progress(task.completion),
135
+ escapeHtml(label(task.roadmapSource || "unknown")),
136
+ ]));
137
+ }
138
+
139
+ function riskPanel() {
140
+ const warnings = bundle.adoption?.warnings || [];
141
+ const grouped = groupBy(warnings, (item) => item.category);
142
+ return `<section class="panel" style="padding:16px;margin-top:16px">
143
+ <h2>Risks and adoption advice</h2>
144
+ <div class="risk-list">${Object.entries(grouped).map(([category, items]) => `
145
+ <div class="risk-item"><strong>${escapeHtml(category)}</strong><p class="muted">${items.length} items</p></div>
146
+ `).join("") || `<p class="empty">No advice</p>`}</div>
147
+ </section>`;
148
+ }
149
+
150
+ function taskTable() {
151
+ const rows = bundle.status?.tasks || [];
152
+ return tablePanel(t("tasks"), [t("task"), t("state"), t("completion"), t("evidence"), t("roadmapSource")], rows.map((task) => [
153
+ clickable(task.title, "task", task.id),
154
+ tag(task.state, label(task.state)),
155
+ progress(task.completion),
156
+ progress(evidenceHealth([task])),
157
+ escapeHtml(label(task.roadmapSource || "unknown")),
158
+ ]));
159
+ }
160
+
161
+ function ledgerSummary() {
162
+ const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "harness-ledger");
163
+ if (tables.length === 0) return "";
164
+ const rows = tables.flatMap((table) => table.rows).slice(0, 5);
165
+ return `<section class="panel ledger-summary">
166
+ <div class="panel-head"><h2>${t("ledger")}</h2><span class="muted">${rows.length}</span></div>
167
+ <div class="risk-list">${rows.map((row) => {
168
+ const cells = row.cells || {};
169
+ const title = cells.Task || cells.ID || cells.Item || cells.Title || cells.Module || "Ledger item";
170
+ const state = cells.State || cells.Status || cells.Review || cells["Review State"] || "";
171
+ const action = cells["Next Action"] || cells.Action || cells.Owner || cells["Required Action"] || "";
172
+ return `<div class="risk-item"><strong>${escapeHtml(title)}</strong><p class="muted">${escapeHtml([label(state), action].filter(Boolean).join(" · "))}</p></div>`;
173
+ }).join("")}</div>
174
+ </section>`;
175
+ }
176
+
177
+ function ledgerTable() {
178
+ const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "harness-ledger");
179
+ if (tables.length === 0) return emptyTable(t("ledger"), "No Harness Ledger found.");
180
+ return genericTables(t("ledger"), tables);
181
+ }
182
+
183
+ function moduleTable() {
184
+ const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "module-registry");
185
+ const graph = graphPanel();
186
+ if (tables.length === 0) return `${graph}${emptyTable(t("modules"), "No Module Registry found.")}`;
187
+ return `${graph}${genericTables(t("modules"), tables)}`;
188
+ }
189
+
190
+ function evidenceTable() {
191
+ const items = evidenceItems();
192
+ return tablePanel(t("evidence"), [t("origin"), t("task"), t("state"), t("title"), t("affected")], items.map((item) => [
193
+ escapeHtml(label(item.source)),
194
+ escapeHtml(item.task || "-"),
195
+ tag(item.state || "present", label(item.state || "present")),
196
+ escapeHtml(item.title),
197
+ escapeHtml(item.affected || ""),
198
+ ]));
199
+ }
200
+
201
+ function evidenceItems() {
202
+ const taskEvidence = (bundle.status?.tasks || []).flatMap((task) => [
203
+ ...(task.evidence || []).map((item) => ({
204
+ source: item.type || "task-progress",
205
+ task: task.title,
206
+ state: item.status || "present",
207
+ title: item.summary || item.id,
208
+ affected: item.path || "",
209
+ })),
210
+ ...(task.risks || []).map((item) => ({
211
+ source: "task-review",
212
+ task: task.title,
213
+ state: item.open ? "open" : "closed",
214
+ title: item.summary || item.id,
215
+ affected: `${item.severity || ""}${item.blocksRelease ? " · blocks" : ""}`.trim(),
216
+ })),
217
+ ]);
218
+ const qaTables = (bundle.tables?.tables || [])
219
+ .filter((table) => ["task-review", "regression-ssot", "cadence-ledger"].includes(table.kind))
220
+ .flatMap((table) => table.rows.slice(0, 12).map((row) => {
221
+ const cells = row.cells || {};
222
+ return {
223
+ source: table.kind,
224
+ task: cells.Task || cells.Module || cells.ID || "-",
225
+ state: cells.Status || cells.State || cells.Open || cells.Verdict || "present",
226
+ title: cells.Finding || cells.Summary || cells.Check || cells.Item || cells.Title || table.source,
227
+ affected: cells["Evidence Checked"] || cells.Path || cells.Owner || cells["Required Action"] || table.source,
228
+ };
229
+ }));
230
+ return [...taskEvidence, ...qaTables];
231
+ }
232
+
233
+ function lessonsTable() {
234
+ const tables = (bundle.tables?.tables || []).filter((table) => table.kind === "lessons-ssot");
235
+ if (tables.length === 0) return emptyTable(t("lessons"), "No Lessons SSoT found.");
236
+ return genericTables(t("lessons"), tables);
237
+ }
238
+
239
+ function adoption() {
240
+ const advice = bundle.adoption?.warnings || [];
241
+ return `<section class="page-grid">
242
+ <div>
243
+ ${tablePanel(t("adoption"), [t("category"), t("severity"), t("title"), t("affected"), t("requiredAction")], advice.map((item) => [
244
+ escapeHtml(item.category),
245
+ tag(item.severity, label(item.severity)),
246
+ escapeHtml(item.title),
247
+ escapeHtml(item.affected),
248
+ escapeHtml(item.requiredAction),
249
+ ]))}
250
+ <section class="panel" style="padding:16px;margin-top:16px">
251
+ <h2>${t("nextAction")}</h2>
252
+ <ol>${((bundle.adoption?.manualSteps || {})[state.lang] || []).map((step) => `<li>${escapeHtml(step)}</li>`).join("")}</ol>
253
+ </section>
254
+ </div>
255
+ ${drawer()}
256
+ </section>`;
257
+ }
258
+
259
+ function settings() {
260
+ return `<section class="panel" style="padding:18px">
261
+ <h2>${t("settings")}</h2>
262
+ <p class="muted">These settings are browser-local and never write back to the project.</p>
263
+ <p>${t("rendered")} / ${t("source")}: Switch inside the detail drawer.</p>
264
+ </section>`;
265
+ }
266
+
267
+ function withDrawer(content) {
268
+ return `<section class="page-grid"><div>${content}</div>${drawer()}</section>`;
269
+ }
270
+
271
+ function tablePanel(title, headers, rows) {
272
+ return `<section class="table-panel">
273
+ <div class="panel-head"><h2>${escapeHtml(title)}</h2><span class="muted">${rows.length}</span></div>
274
+ <div class="table-wrap"><table>
275
+ <thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead>
276
+ <tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody>
277
+ </table></div>
278
+ </section>`;
279
+ }
280
+
281
+ function genericTables(title, tables) {
282
+ return tables.map((table) => tablePanel(`${title} · ${table.source}`, table.columns, table.rows.map((row) => table.columns.map((column) => escapeHtml(row.cells[column] || ""))))).join("");
283
+ }
284
+
285
+ function graphPanel() {
286
+ const graph = bundle.graph || { nodes: [], edges: [] };
287
+ const modules = graph.nodes.filter((node) => node.type === "module");
288
+ const fallbackTasks = graph.nodes.filter((node) => node.type === "task");
289
+ const laneNodes = modules.length > 0 ? modules : fallbackTasks;
290
+ const edges = graph.edges || [];
291
+ return `<section class="panel graph-panel">
292
+ <div class="panel-head"><h2>${t("graph")}</h2><span class="muted">${modules.length} modules · ${fallbackTasks.length} tasks · ${edges.length} edges</span></div>
293
+ <div class="graph-lanes">${laneNodes.slice(0, 12).map((module) => {
294
+ const owned = edges.filter((edge) => edge.from === module.id).slice(0, 8);
295
+ return `<div class="lane"><strong>${escapeHtml(module.label)}</strong><span>${label(module.state || "")}</span>${owned.map((edge) => `<small>${escapeHtml(edge.type)} → ${escapeHtml(edge.to.replace(/^step:/, ""))}</small>`).join("")}</div>`;
296
+ }).join("") || `<p class="empty">No module graph data</p>`}</div>
297
+ </section>`;
298
+ }
299
+
300
+ function emptyTable(title, message) {
301
+ return `<section class="table-panel"><div class="panel-head"><h2>${escapeHtml(title)}</h2></div><p class="empty">${escapeHtml(message)}</p></section>`;
302
+ }
303
+
304
+ function drawer() {
305
+ const selection = state.selected;
306
+ if (!selection) {
307
+ return `<aside class="drawer"><div class="drawer-head"><h2>${t("detail")}</h2></div><div class="drawer-body empty">${t("noSelection")}</div></aside>`;
308
+ }
309
+ const docs = docsForSelection(selection);
310
+ const active = docs.find((doc) => doc.tab === state.tab) || docs[0];
311
+ return `<aside class="drawer">
312
+ <div class="drawer-head"><h2>${escapeHtml(selection.label)}</h2><p class="muted">${escapeHtml(active?.path || "")}</p></div>
313
+ <div class="drawer-tabs">${docs.map((doc) => `<button data-tab="${doc.tab}" class="${active?.tab === doc.tab ? "active" : ""}">${t(doc.tab)}</button>`).join("")}</div>
314
+ <div class="drawer-mode"><button data-render-mode="rendered" class="${state.renderMode === "rendered" ? "active" : ""}">${t("rendered")}</button><button data-render-mode="source" class="${state.renderMode === "source" ? "active" : ""}">${t("source")}</button></div>
315
+ <div class="drawer-body"><article class="markdown">${active ? window.HarnessMarkdown.render(active.content, state.renderMode) : t("noSelection")}</article></div>
316
+ </aside>`;
317
+ }
318
+
319
+ function docsForSelection(selection) {
320
+ if (selection.type !== "task") return [];
321
+ const task = (bundle.status?.tasks || []).find((item) => item.id === selection.id);
322
+ if (!task) return [];
323
+ return taskDocTabs.map(([tab, suffix]) => {
324
+ const doc = findDocument(`${task.path}/${suffix}`);
325
+ return doc ? { tab, ...doc } : null;
326
+ }).filter(Boolean);
327
+ }
328
+
329
+ function findDocument(pathSuffix) {
330
+ return (bundle.documents?.documents || []).find((doc) => doc.path.endsWith(pathSuffix));
331
+ }
332
+
333
+ function clickable(label, type, id) {
334
+ return `<button class="linklike" data-select-type="${type}" data-select-id="${id}" data-select-label="${escapeAttr(label)}">${escapeHtml(label)}</button>`;
335
+ }
336
+
337
+ function tag(value, text = value) {
338
+ const raw = String(value || "unknown");
339
+ const klass = /fail|blocked|open/i.test(raw) ? "fail" : /warn|advice|planned|missing/i.test(raw) ? "warn" : /pass|done|present|verified/i.test(raw) ? "pass" : "";
340
+ return `<span class="tag ${klass}">${escapeHtml(text)}</span>`;
341
+ }
342
+
343
+ function label(value) {
344
+ const labels = {
345
+ pass: "pass",
346
+ warn: "warn",
347
+ fail: "fail",
348
+ in_progress: "in progress",
349
+ planned: "planned",
350
+ done: "done",
351
+ blocked: "blocked",
352
+ missing: "missing",
353
+ present: "present",
354
+ closed: "closed",
355
+ advice: "advice",
356
+ standalone: "standalone",
357
+ legacy: "legacy",
358
+ "task-review": "review",
359
+ "task-progress": "progress",
360
+ "regression-ssot": "regression",
361
+ "cadence-ledger": "cadence",
362
+ unknown: "unknown",
363
+ };
364
+ return labels[value] || value;
365
+ }
366
+
367
+ function progress(value) {
368
+ const score = Math.max(0, Math.min(100, Number(value) || 0));
369
+ return `<span>${score}%</span><div class="bar"><i style="width:${score}%"></i></div>`;
370
+ }
371
+
372
+ function evidenceHealth(tasks) {
373
+ const phases = tasks.flatMap((task) => task.phases || []).filter((phase) => phase.state !== "skipped");
374
+ if (phases.length === 0) return 0;
375
+ const score = phases.reduce((sum, phase) => {
376
+ if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
377
+ if (phase.evidenceStatus === "partial") return sum + 50;
378
+ return sum;
379
+ }, 0);
380
+ return Math.round(score / phases.length);
381
+ }
382
+
383
+ function groupBy(items, fn) {
384
+ return items.reduce((acc, item) => {
385
+ const key = fn(item);
386
+ acc[key] ||= [];
387
+ acc[key].push(item);
388
+ return acc;
389
+ }, {});
390
+ }
391
+
392
+ function bind() {
393
+ document.querySelectorAll("[data-page]").forEach((button) => button.addEventListener("click", () => {
394
+ state.page = button.dataset.page;
395
+ state.selected = null;
396
+ app();
397
+ }));
398
+ document.querySelectorAll("[data-theme]").forEach((button) => button.addEventListener("click", () => {
399
+ state.theme = button.dataset.theme;
400
+ localStorage.setItem("harness.theme", state.theme);
401
+ app();
402
+ }));
403
+ document.querySelectorAll("[data-density]").forEach((button) => button.addEventListener("click", () => {
404
+ state.density = button.dataset.density;
405
+ localStorage.setItem("harness.density", state.density);
406
+ app();
407
+ }));
408
+ document.querySelectorAll("[data-select-type]").forEach((button) => button.addEventListener("click", () => {
409
+ state.selected = { type: button.dataset.selectType, id: button.dataset.selectId, label: button.dataset.selectLabel };
410
+ state.tab = "plan";
411
+ app();
412
+ }));
413
+ document.querySelectorAll("[data-tab]").forEach((button) => button.addEventListener("click", () => {
414
+ state.tab = button.dataset.tab;
415
+ app();
416
+ }));
417
+ document.querySelectorAll("[data-render-mode]").forEach((button) => button.addEventListener("click", () => {
418
+ state.renderMode = button.dataset.renderMode;
419
+ app();
420
+ }));
421
+ }
422
+
423
+ function escapeHtml(value) {
424
+ return String(value ?? "")
425
+ .replaceAll("&", "&amp;")
426
+ .replaceAll("<", "&lt;")
427
+ .replaceAll(">", "&gt;")
428
+ .replaceAll('"', "&quot;");
429
+ }
430
+
431
+ function escapeAttr(value) {
432
+ return escapeHtml(value).replaceAll("'", "&#39;");
433
+ }
434
+
435
+ app();
@@ -0,0 +1,47 @@
1
+ window.HarnessI18n = {
2
+ en: {
3
+ overview: "Overview",
4
+ ledger: "Ledger",
5
+ tasks: "Tasks",
6
+ modules: "Modules",
7
+ evidence: "Evidence",
8
+ lessons: "Lessons",
9
+ adoption: "Adoption",
10
+ settings: "Settings",
11
+ system: "System",
12
+ readiness: "Readiness",
13
+ blockers: "Blockers",
14
+ warnings: "Advice",
15
+ nextAction: "Next action",
16
+ activeTasks: "Active tasks",
17
+ evidenceHealth: "Evidence health",
18
+ detail: "Detail",
19
+ plan: "Plan",
20
+ strategy: "Execution strategy",
21
+ roadmap: "Visual roadmap",
22
+ progress: "Progress",
23
+ review: "Review",
24
+ findings: "Findings",
25
+ references: "References",
26
+ artifacts: "Artifacts",
27
+ source: "Source",
28
+ rendered: "Rendered",
29
+ light: "Light",
30
+ dark: "Dark",
31
+ compact: "Compact",
32
+ comfortable: "Comfortable",
33
+ noSelection: "Select a row to inspect the document snapshot",
34
+ task: "Task",
35
+ state: "State",
36
+ completion: "Completion",
37
+ roadmapSource: "Roadmap",
38
+ category: "Category",
39
+ severity: "Severity",
40
+ title: "Title",
41
+ affected: "Affected",
42
+ requiredAction: "Required action",
43
+ message: "Detail",
44
+ graph: "Graph",
45
+ origin: "Source",
46
+ },
47
+ };
@@ -0,0 +1,116 @@
1
+ window.HarnessMarkdown = {
2
+ render(markdown = "", mode = "rendered") {
3
+ if (mode === "source") return `<pre><code>${escapeHtml(markdown)}</code></pre>`;
4
+ return renderBlocks(markdown);
5
+ },
6
+ };
7
+
8
+ function renderBlocks(markdown) {
9
+ const lines = String(markdown || "").split(/\r?\n/);
10
+ const blocks = [];
11
+ let index = 0;
12
+ while (index < lines.length) {
13
+ const line = lines[index];
14
+ if (!line.trim()) {
15
+ index += 1;
16
+ continue;
17
+ }
18
+ if (line.startsWith("```")) {
19
+ const lang = line.slice(3).trim();
20
+ const code = [];
21
+ index += 1;
22
+ while (index < lines.length && !lines[index].startsWith("```")) {
23
+ code.push(lines[index]);
24
+ index += 1;
25
+ }
26
+ index += 1;
27
+ blocks.push(lang === "mermaid" ? window.HarnessMermaid.render(code.join("\n")) : `<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
28
+ continue;
29
+ }
30
+ if (/^#{1,3}\s+/.test(line)) {
31
+ const level = line.match(/^#+/)[0].length;
32
+ blocks.push(`<h${level}>${inline(line.replace(/^#{1,3}\s+/, ""))}</h${level}>`);
33
+ index += 1;
34
+ continue;
35
+ }
36
+ if (isTableStart(lines, index)) {
37
+ const table = [];
38
+ while (index < lines.length && lines[index].trim().startsWith("|")) {
39
+ table.push(lines[index]);
40
+ index += 1;
41
+ }
42
+ blocks.push(renderTable(table));
43
+ continue;
44
+ }
45
+ if (/^[-*]\s+/.test(line)) {
46
+ const items = [];
47
+ while (index < lines.length && /^[-*]\s+/.test(lines[index])) {
48
+ items.push(`<li>${inline(lines[index].replace(/^[-*]\s+/, ""))}</li>`);
49
+ index += 1;
50
+ }
51
+ blocks.push(`<ul>${items.join("")}</ul>`);
52
+ continue;
53
+ }
54
+ const paragraph = [];
55
+ while (index < lines.length && lines[index].trim() && !/^#{1,3}\s+/.test(lines[index]) && !lines[index].startsWith("```") && !isTableStart(lines, index)) {
56
+ paragraph.push(lines[index]);
57
+ index += 1;
58
+ }
59
+ blocks.push(`<p>${inline(paragraph.join(" "))}</p>`);
60
+ }
61
+ return blocks.join("");
62
+ }
63
+
64
+ function isTableStart(lines, index) {
65
+ return lines[index]?.trim().startsWith("|") && /^(\s*\|?\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[index + 1] || "");
66
+ }
67
+
68
+ function renderTable(lines) {
69
+ const rows = lines.map(splitMarkdownRow);
70
+ const header = rows[0] || [];
71
+ const body = rows.slice(2).filter((row) => row.length === header.length);
72
+ return `<div class="rendered-table-wrap"><table class="rendered-table">
73
+ <thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>
74
+ <tbody>${body.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell)}</td>`).join("")}</tr>`).join("")}</tbody>
75
+ </table></div>`;
76
+ }
77
+
78
+ function splitMarkdownRow(line) {
79
+ let text = String(line || "").trim();
80
+ if (text.startsWith("|")) text = text.slice(1);
81
+ if (text.endsWith("|") && !text.endsWith("\\|")) text = text.slice(0, -1);
82
+ const cells = [];
83
+ let current = "";
84
+ let inCode = false;
85
+ for (let index = 0; index < text.length; index += 1) {
86
+ const char = text[index];
87
+ if (char === "\\" && text[index + 1] === "|") {
88
+ current += "|";
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (char === "`") inCode = !inCode;
93
+ if (char === "|" && !inCode) {
94
+ cells.push(current.trim());
95
+ current = "";
96
+ continue;
97
+ }
98
+ current += char;
99
+ }
100
+ cells.push(current.trim());
101
+ return cells;
102
+ }
103
+
104
+ function inline(value) {
105
+ return escapeHtml(value)
106
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
107
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
108
+ }
109
+
110
+ function escapeHtml(value) {
111
+ return String(value ?? "")
112
+ .replaceAll("&", "&amp;")
113
+ .replaceAll("<", "&lt;")
114
+ .replaceAll(">", "&gt;")
115
+ .replaceAll('"', "&quot;");
116
+ }
@@ -0,0 +1,59 @@
1
+ window.HarnessMermaid = {
2
+ render(code = "") {
3
+ const parsed = parseFlowchart(code);
4
+ if (!parsed) {
5
+ return `<figure class="mermaid-fallback">
6
+ <figcaption>Mermaid source</figcaption>
7
+ <pre>${escapeMermaid(code)}</pre>
8
+ </figure>`;
9
+ }
10
+ return renderSvg(parsed);
11
+ },
12
+ };
13
+
14
+ function parseFlowchart(code) {
15
+ const lines = String(code || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
16
+ if (!/^flowchart\s+(LR|TB|TD|RL|BT)/i.test(lines[0] || "")) return null;
17
+ const nodes = new Map();
18
+ const edges = [];
19
+ for (const line of lines.slice(1)) {
20
+ const edgeMatch = line.match(/^([A-Za-z0-9_-]+)(?:\["([^"]+)"\])?\s*-+>\s*([A-Za-z0-9_-]+)(?:\["([^"]+)"\])?/);
21
+ if (!edgeMatch) continue;
22
+ const [, from, fromLabel, to, toLabel] = edgeMatch;
23
+ nodes.set(from, fromLabel || from);
24
+ nodes.set(to, toLabel || to);
25
+ edges.push([from, to]);
26
+ }
27
+ if (nodes.size === 0) return null;
28
+ return { nodes: [...nodes.entries()], edges };
29
+ }
30
+
31
+ function renderSvg({ nodes, edges }) {
32
+ const width = Math.max(360, nodes.length * 150);
33
+ const height = 150;
34
+ const positions = new Map(nodes.map(([id], index) => [id, { x: 70 + index * 140, y: 70 }]));
35
+ const nodeSvg = nodes.map(([id, label]) => {
36
+ const pos = positions.get(id);
37
+ return `<g class="mermaid-node">
38
+ <rect x="${pos.x - 54}" y="${pos.y - 24}" width="108" height="48" rx="8"></rect>
39
+ <text x="${pos.x}" y="${pos.y + 4}" text-anchor="middle">${escapeMermaid(label).replaceAll("\\n", " ")}</text>
40
+ </g>`;
41
+ }).join("");
42
+ const edgeSvg = edges.map(([from, to]) => {
43
+ const a = positions.get(from);
44
+ const b = positions.get(to);
45
+ if (!a || !b) return "";
46
+ return `<path class="mermaid-edge" d="M ${a.x + 54} ${a.y} L ${b.x - 62} ${b.y}"></path><path class="mermaid-arrow" d="M ${b.x - 62} ${b.y} l -7 -5 v 10 z"></path>`;
47
+ }).join("");
48
+ return `<figure class="mermaid-rendered">
49
+ <figcaption>Mermaid</figcaption>
50
+ <svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Mermaid flowchart">${edgeSvg}${nodeSvg}</svg>
51
+ </figure>`;
52
+ }
53
+
54
+ function escapeMermaid(value) {
55
+ return String(value ?? "")
56
+ .replaceAll("&", "&amp;")
57
+ .replaceAll("<", "&lt;")
58
+ .replaceAll(">", "&gt;");
59
+ }
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Harness Dashboard</title>
7
+ <link rel="icon" href="data:,">
8
+ <link rel="stylesheet" href="./assets/app.css">
9
+ </head>
10
+ <body>
11
+ <div id="app" class="app-shell" aria-live="polite"></div>
12
+ <script src="./assets/dashboard-data.js"></script>
13
+ <script src="./assets/i18n.js"></script>
14
+ <script src="./assets/markdown-reader.js"></script>
15
+ <script src="./assets/mermaid-renderer.js"></script>
16
+ <script src="./assets/app.js"></script>
17
+ </body>
18
+ </html>