coding-agent-harness 1.0.1 → 1.0.2

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 (159) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.en-US.md +14 -0
  3. package/README.md +111 -86
  4. package/README.zh-CN.md +270 -0
  5. package/SKILL.md +116 -189
  6. package/docs-release/README.md +72 -5
  7. package/docs-release/architecture/overview.md +286 -28
  8. package/docs-release/architecture/overview.zh-CN.md +288 -0
  9. package/docs-release/assets/dashboard-overview-en.png +0 -0
  10. package/docs-release/assets/harness-architecture.svg +163 -0
  11. package/docs-release/assets/harness-workflow.svg +64 -0
  12. package/docs-release/guides/agent-installation.en-US.md +214 -0
  13. package/docs-release/guides/agent-installation.md +123 -26
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
  15. package/docs-release/guides/document-audience-and-surfaces.md +112 -0
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
  20. package/docs-release/guides/migration-playbook.en-US.md +325 -0
  21. package/docs-release/guides/migration-playbook.md +329 -0
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
  23. package/docs-release/guides/parent-control-repository-pattern.md +252 -0
  24. package/docs-release/guides/repository-operating-models.en-US.md +196 -0
  25. package/docs-release/guides/repository-operating-models.md +196 -0
  26. package/docs-release/intl/README.md +15 -0
  27. package/docs-release/intl/de-DE.md +18 -0
  28. package/docs-release/intl/en-US.md +18 -0
  29. package/docs-release/intl/es-ES.md +18 -0
  30. package/docs-release/intl/fr-FR.md +18 -0
  31. package/docs-release/intl/ja-JP.md +18 -0
  32. package/docs-release/intl/ko-KR.md +18 -0
  33. package/docs-release/intl/zh-CN.md +18 -0
  34. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
  35. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
  36. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
  37. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
  38. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
  39. package/package.json +3 -1
  40. package/references/agents-md-pattern.md +3 -3
  41. package/references/docs-directory-standard.md +47 -3
  42. package/references/external-source-intake-standard.md +75 -0
  43. package/references/harness-ledger.md +5 -3
  44. package/references/legacy-12-phase-bootstrap.md +41 -0
  45. package/references/lessons-governance.md +23 -6
  46. package/references/planning-loop.md +41 -3
  47. package/references/project-onboarding-audit.md +10 -0
  48. package/references/repo-governance-standard.md +2 -0
  49. package/references/testing-standard.md +50 -0
  50. package/references/walkthrough-closeout.md +6 -5
  51. package/scripts/check-harness.mjs +76 -35
  52. package/scripts/harness.mjs +303 -12
  53. package/scripts/lib/capability-registry.mjs +533 -0
  54. package/scripts/lib/check-profiles.mjs +510 -0
  55. package/scripts/lib/core-shared.mjs +186 -0
  56. package/scripts/lib/dashboard-data.mjs +389 -0
  57. package/scripts/lib/dashboard-workbench.mjs +217 -0
  58. package/scripts/lib/dashboard-writer.mjs +93 -2
  59. package/scripts/lib/harness-core.mjs +10 -1318
  60. package/scripts/lib/lesson-maintenance.mjs +145 -0
  61. package/scripts/lib/markdown-utils.mjs +158 -0
  62. package/scripts/lib/migration-planner.mjs +478 -0
  63. package/scripts/lib/migration-support.mjs +312 -0
  64. package/scripts/lib/task-lifecycle.mjs +755 -0
  65. package/scripts/lib/task-scanner.mjs +682 -0
  66. package/scripts/smoke-dashboard.mjs +22 -0
  67. package/scripts/test-harness.mjs +926 -14
  68. package/templates/AGENTS.md.template +41 -30
  69. package/templates/architecture/Architecture-SSoT.md +21 -0
  70. package/templates/architecture/README.md +49 -0
  71. package/templates/architecture/critical-flows.md +22 -0
  72. package/templates/architecture/local-repo-context.md +20 -0
  73. package/templates/architecture/service-catalog.md +17 -0
  74. package/templates/architecture/services/service-template.md +31 -0
  75. package/templates/architecture/system-map.md +22 -0
  76. package/templates/dashboard/assets/app-src/00-state.js +41 -0
  77. package/templates/dashboard/assets/app-src/10-router.js +76 -0
  78. package/templates/dashboard/assets/app-src/20-overview.js +235 -0
  79. package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
  80. package/templates/dashboard/assets/app-src/40-modules.js +58 -0
  81. package/templates/dashboard/assets/app-src/45-review.js +128 -0
  82. package/templates/dashboard/assets/app-src/50-migration.js +169 -0
  83. package/templates/dashboard/assets/app-src/60-shared.js +61 -0
  84. package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
  85. package/templates/dashboard/assets/app.css +2575 -310
  86. package/templates/dashboard/assets/app.js +1498 -307
  87. package/templates/dashboard/assets/app.manifest.json +11 -0
  88. package/templates/dashboard/assets/i18n.js +429 -44
  89. package/templates/dashboard/assets/mermaid-renderer.js +58 -8
  90. package/templates/development/README.md +52 -0
  91. package/templates/development/codebase-map.md +11 -0
  92. package/templates/development/cross-repo-debugging.md +18 -0
  93. package/templates/development/external-context/service-template.md +33 -0
  94. package/templates/development/external-source-packs/README.md +24 -0
  95. package/templates/development/external-source-packs/digest-template.md +28 -0
  96. package/templates/development/local-setup.md +16 -0
  97. package/templates/development/stubs-and-mocks.md +11 -0
  98. package/templates/integrations/README.md +40 -0
  99. package/templates/integrations/api-contract.md +42 -0
  100. package/templates/integrations/event-contract.md +46 -0
  101. package/templates/integrations/third-party/vendor-template.md +42 -0
  102. package/templates/integrations/webhook-contract.md +41 -0
  103. package/templates/planning/brief.md +32 -0
  104. package/templates/planning/lesson_candidates.md +58 -0
  105. package/templates/planning/long-running-task-contract.md +7 -0
  106. package/templates/planning/module_brief.md +25 -0
  107. package/templates/planning/module_session_prompt.md +6 -0
  108. package/templates/planning/task_plan.md +7 -5
  109. package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
  110. package/templates/reference/docs-library-standard.md +31 -0
  111. package/templates/reference/execution-workflow-standard.md +4 -2
  112. package/templates/reference/external-source-intake-standard.md +82 -0
  113. package/templates/reference/harness-ledger-standard.md +1 -0
  114. package/templates/reference/repo-governance-standard.md +6 -4
  115. package/templates/reference/walkthrough-standard.md +2 -1
  116. package/templates/walkthrough/walkthrough-template.md +2 -2
  117. package/templates-zh-CN/AGENTS.md.template +69 -70
  118. package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
  119. package/templates-zh-CN/architecture/README.md +51 -0
  120. package/templates-zh-CN/architecture/critical-flows.md +24 -0
  121. package/templates-zh-CN/architecture/local-repo-context.md +20 -0
  122. package/templates-zh-CN/architecture/service-catalog.md +17 -0
  123. package/templates-zh-CN/architecture/services/service-template.md +31 -0
  124. package/templates-zh-CN/architecture/system-map.md +22 -0
  125. package/templates-zh-CN/development/README.md +54 -0
  126. package/templates-zh-CN/development/codebase-map.md +11 -0
  127. package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
  128. package/templates-zh-CN/development/external-context/service-template.md +33 -0
  129. package/templates-zh-CN/development/external-source-packs/README.md +24 -0
  130. package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
  131. package/templates-zh-CN/development/local-setup.md +16 -0
  132. package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
  133. package/templates-zh-CN/integrations/README.md +42 -0
  134. package/templates-zh-CN/integrations/api-contract.md +42 -0
  135. package/templates-zh-CN/integrations/event-contract.md +46 -0
  136. package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
  137. package/templates-zh-CN/integrations/webhook-contract.md +41 -0
  138. package/templates-zh-CN/planning/brief.md +32 -0
  139. package/templates-zh-CN/planning/lesson_candidates.md +58 -0
  140. package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
  141. package/templates-zh-CN/planning/module_brief.md +25 -0
  142. package/templates-zh-CN/planning/module_plan.md +2 -2
  143. package/templates-zh-CN/planning/module_session_prompt.md +4 -3
  144. package/templates-zh-CN/planning/task_plan.md +10 -4
  145. package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
  146. package/templates-zh-CN/reference/docs-library-standard.md +35 -0
  147. package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
  148. package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
  149. package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
  150. package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
  151. package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
  152. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
  153. package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
  154. package/templates-zh-CN/dashboard/assets/app.css +0 -399
  155. package/templates-zh-CN/dashboard/assets/app.js +0 -435
  156. package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
  157. package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
  158. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
  159. package/templates-zh-CN/dashboard/index.html +0 -18
@@ -0,0 +1,217 @@
1
+ import crypto from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import http from "node:http";
5
+ import path from "node:path";
6
+ import { URL } from "node:url";
7
+ import { confirmTaskReview } from "./task-lifecycle.mjs";
8
+ import { normalizeTarget } from "./core-shared.mjs";
9
+ import { collectTasks } from "./task-scanner.mjs";
10
+ import { writeDashboardFolder } from "./dashboard-data.mjs";
11
+
12
+ const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
13
+
14
+ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench" } = {}) {
15
+ if (host !== "127.0.0.1") throw new Error("dashboard workbench only supports --host 127.0.0.1");
16
+ const target = normalizeTarget(targetInput);
17
+ const outputDir = path.resolve(outDir);
18
+ const csrfToken = crypto.randomBytes(24).toString("hex");
19
+ const options = localeOverride ? { localeOverride } : {};
20
+ let snapshotVersion = Date.now();
21
+ const regenerate = () => {
22
+ writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true });
23
+ snapshotVersion = Date.now();
24
+ };
25
+ regenerate();
26
+
27
+ const server = http.createServer(async (request, response) => {
28
+ try {
29
+ const address = server.address();
30
+ const actualPort = typeof address === "object" && address ? address.port : port;
31
+ const origin = `http://${host}:${actualPort}`;
32
+ const requestUrl = new URL(request.url || "/", origin);
33
+
34
+ if (requestUrl.pathname === "/api/runtime" && request.method === "GET") {
35
+ writeJson(response, 200, {
36
+ mode: "workbench",
37
+ csrfToken,
38
+ writableActions: ["review-complete"],
39
+ target: target.projectRoot,
40
+ autoRefresh: autoRefresh === true,
41
+ snapshotVersion,
42
+ });
43
+ return;
44
+ }
45
+
46
+ if (requestUrl.pathname === "/api/tasks/review-complete" && request.method === "POST") {
47
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
48
+ const body = await readJsonBody(request);
49
+ const taskId = String(body.taskId || "");
50
+ const task = collectTasks(target).find((item) => item.id === taskId);
51
+ if (!task) {
52
+ writeJson(response, 404, { error: "Task not found" });
53
+ return;
54
+ }
55
+ if (!isTaskInReviewStage(task)) {
56
+ writeJson(response, 409, { error: "Review completion is only available while the task is in review." });
57
+ return;
58
+ }
59
+ if (task.reviewStatus === "confirmed") {
60
+ writeJson(response, 409, { error: "Review is already confirmed." });
61
+ return;
62
+ }
63
+ const result = confirmTaskReview(target.projectRoot, taskId, {
64
+ reviewer: body.reviewer || "Human Reviewer",
65
+ message: body.message || "confirmed from dashboard workbench",
66
+ evidence: body.evidence || "",
67
+ confirmText: body.confirmText || "",
68
+ });
69
+ regenerate();
70
+ writeJson(response, 200, result);
71
+ return;
72
+ }
73
+
74
+ if (request.method !== "GET" && request.method !== "HEAD") {
75
+ writeJson(response, 405, { error: "Method not allowed" });
76
+ return;
77
+ }
78
+ serveStaticFile(response, outputDir, requestUrl.pathname, request.method === "HEAD");
79
+ } catch (error) {
80
+ const status = /CSRF|Origin|Host/.test(error.message) ? 403 : 400;
81
+ writeJson(response, status, { error: error.message });
82
+ }
83
+ });
84
+
85
+ await new Promise((resolve, reject) => {
86
+ server.once("error", reject);
87
+ server.listen(Number(port), host, resolve);
88
+ });
89
+ const address = server.address();
90
+ const actualPort = typeof address === "object" && address ? address.port : port;
91
+ const url = `http://${host}:${actualPort}/`;
92
+ let watcher = null;
93
+ if (autoRefresh) watcher = startPollingWatch(target.docsRoot, regenerate);
94
+ console.log(`${label}: ${url} csrf=${csrfToken} outDir=${outputDir}`);
95
+ if (open) openBrowser(url);
96
+
97
+ const close = () => {
98
+ if (watcher) clearInterval(watcher);
99
+ server.close(() => process.exit(0));
100
+ };
101
+ process.once("SIGINT", close);
102
+ process.once("SIGTERM", close);
103
+ await new Promise(() => {});
104
+ }
105
+
106
+ function isTaskInReviewStage(task) {
107
+ const state = task?.state || "";
108
+ const lifecycle = task?.lifecycleState || "";
109
+ if (["not_started", "planned", "in_progress"].includes(state)) return false;
110
+ return state === "review" || ["in_review", "review-blocked"].includes(lifecycle);
111
+ }
112
+
113
+ function startPollingWatch(root, regenerate) {
114
+ let lastMtime = latestTreeMtime(root);
115
+ let timer = null;
116
+ return setInterval(() => {
117
+ const nextMtime = latestTreeMtime(root);
118
+ if (nextMtime <= lastMtime) return;
119
+ lastMtime = nextMtime;
120
+ clearTimeout(timer);
121
+ timer = setTimeout(() => {
122
+ try {
123
+ regenerate();
124
+ } catch (error) {
125
+ console.error(`dashboard regeneration failed: ${error.message}`);
126
+ }
127
+ }, 250);
128
+ }, 1000);
129
+ }
130
+
131
+ function latestTreeMtime(root) {
132
+ let latest = 0;
133
+ if (!fs.existsSync(root)) return latest;
134
+ const visit = (dir) => {
135
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
136
+ if ([".git", "node_modules", "tmp"].includes(entry.name)) continue;
137
+ const fullPath = path.join(dir, entry.name);
138
+ const stat = fs.statSync(fullPath);
139
+ latest = Math.max(latest, stat.mtimeMs);
140
+ if (entry.isDirectory()) visit(fullPath);
141
+ }
142
+ };
143
+ visit(root);
144
+ return latest;
145
+ }
146
+
147
+ function openBrowser(url) {
148
+ const command =
149
+ process.platform === "darwin" ? "open" :
150
+ process.platform === "win32" ? "cmd" :
151
+ "xdg-open";
152
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
153
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
154
+ child.on("error", () => {});
155
+ child.unref();
156
+ }
157
+
158
+ function assertTrustedWorkbenchRequest(request, { origin, csrfToken }) {
159
+ const host = request.headers.host || "";
160
+ if (host !== origin.replace(/^http:\/\//, "")) throw new Error("Host mismatch");
161
+ if (request.headers.origin !== origin) throw new Error("Origin mismatch");
162
+ if (request.headers["x-harness-csrf"] !== csrfToken) throw new Error("CSRF token mismatch");
163
+ }
164
+
165
+ function readJsonBody(request) {
166
+ return new Promise((resolve, reject) => {
167
+ let raw = "";
168
+ request.setEncoding("utf8");
169
+ request.on("data", (chunk) => {
170
+ raw += chunk;
171
+ if (raw.length > 32_768) {
172
+ reject(new Error("Request body too large"));
173
+ request.destroy();
174
+ }
175
+ });
176
+ request.on("end", () => {
177
+ try {
178
+ resolve(raw ? JSON.parse(raw) : {});
179
+ } catch {
180
+ reject(new Error("Invalid JSON body"));
181
+ }
182
+ });
183
+ request.on("error", reject);
184
+ });
185
+ }
186
+
187
+ function serveStaticFile(response, outputDir, urlPath, headOnly) {
188
+ const decoded = decodeURIComponent(urlPath);
189
+ const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
190
+ const filePath = path.resolve(outputDir, relative);
191
+ if (!isPathInside(filePath, outputDir) || !fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
192
+ writeJson(response, 404, { error: "Not found" });
193
+ return;
194
+ }
195
+ response.writeHead(200, { "content-type": mimeType(filePath), "cache-control": "no-store" });
196
+ if (!headOnly) response.end(fs.readFileSync(filePath));
197
+ else response.end();
198
+ }
199
+
200
+ function writeJson(response, status, payload) {
201
+ response.writeHead(status, jsonHeaders);
202
+ response.end(`${JSON.stringify(payload)}\n`);
203
+ }
204
+
205
+ function mimeType(filePath) {
206
+ if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
207
+ if (filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
208
+ if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
209
+ if (filePath.endsWith(".json")) return "application/json; charset=utf-8";
210
+ if (filePath.endsWith(".md")) return "text/markdown; charset=utf-8";
211
+ return "application/octet-stream";
212
+ }
213
+
214
+ function isPathInside(candidate, parent) {
215
+ const relative = path.relative(path.resolve(parent), path.resolve(candidate));
216
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
217
+ }
@@ -19,6 +19,8 @@ export function writeDashboardDirectory(outDir, bundle, options = {}) {
19
19
  assertSafeDashboardTarget(target, options);
20
20
  if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
21
21
  copyDashboardAssets(target, options);
22
+ fs.writeFileSync(path.join(target, "assets/app.js"), readDashboardApp(dashboardTemplateRootForLocale(options.locale)));
23
+ fs.writeFileSync(path.join(target, "index.html"), renderDashboardIndex(options.locale, options));
22
24
  fs.writeFileSync(path.join(target, dashboardMarker), "generated dashboard directory\n");
23
25
  writeJsonFile(path.join(target, "data/status.json"), bundle.status);
24
26
  writeJsonFile(path.join(target, "data/tables.json"), bundle.tables);
@@ -42,8 +44,98 @@ export function writeDashboardDirectory(outDir, bundle, options = {}) {
42
44
  return target;
43
45
  }
44
46
 
47
+ export function writeDashboardFile(outFile, bundle, options = {}) {
48
+ const target = path.resolve(outFile);
49
+ fs.mkdirSync(path.dirname(target), { recursive: true });
50
+ fs.writeFileSync(target, renderDashboardFile(bundle, options));
51
+ return target;
52
+ }
53
+
54
+ export function renderDashboardFile(bundle, options = {}) {
55
+ const templateRoot = dashboardTemplateRootForLocale(options.locale);
56
+ const readAsset = (relativePath) => fs.readFileSync(path.join(templateRoot, relativePath), "utf8");
57
+ const css = readAsset("assets/app.css");
58
+ const i18n = readAsset("assets/i18n.js");
59
+ const markdown = readAsset("assets/markdown-reader.js");
60
+ const mermaid = readAsset("assets/mermaid-renderer.js");
61
+ const app = readDashboardApp(templateRoot);
62
+ const title = options.locale === "zh-CN" ? "Harness 控制台" : "Harness Dashboard";
63
+ const payload = JSON.stringify(bundle).replace(/</g, "\\u003c");
64
+ const runtimeLocale = options.locale === "zh-CN" ? "zh" : "en";
65
+ return `<!doctype html>
66
+ <html lang="${options.locale === "zh-CN" ? "zh-CN" : "en"}">
67
+ <head>
68
+ <meta charset="utf-8">
69
+ <meta name="viewport" content="width=device-width, initial-scale=1">
70
+ <title>${title}</title>
71
+ <link rel="icon" href="data:,">
72
+ <style>${css}</style>
73
+ </head>
74
+ <body>
75
+ <div id="app" class="app-shell" aria-live="polite"></div>
76
+ <script>window.__HARNESS_DASHBOARD__ = ${payload};</script>
77
+ <script>window.__HARNESS_LOCALE__ = ${JSON.stringify(runtimeLocale)};</script>
78
+ <script>${i18n}</script>
79
+ <script>${markdown}</script>
80
+ <script>${mermaid}</script>
81
+ <script>${app}</script>
82
+ </body>
83
+ </html>`;
84
+ }
85
+
86
+ function renderDashboardIndex(locale = "en-US", options = {}) {
87
+ const normalizedLocale = locale === "zh-CN" ? "zh-CN" : "en";
88
+ const runtimeLocale = locale === "zh-CN" ? "zh" : "en";
89
+ const title = locale === "zh-CN" ? "Harness 控制台" : "Harness Dashboard";
90
+ const assetVersion = Date.now();
91
+ return `<!doctype html>
92
+ <html lang="${normalizedLocale}">
93
+ <head>
94
+ <meta charset="utf-8">
95
+ <meta name="viewport" content="width=device-width, initial-scale=1">
96
+ <title>${title}</title>
97
+ <link rel="icon" href="data:,">
98
+ <link rel="stylesheet" href="./assets/app.css?v=${assetVersion}">
99
+ </head>
100
+ <body>
101
+ <div id="app" class="app-shell" aria-live="polite"></div>
102
+ <script src="./assets/dashboard-data.js?v=${assetVersion}"></script>
103
+ <script>window.__HARNESS_LOCALE__ = ${JSON.stringify(runtimeLocale)};</script>
104
+ <script>window.__HARNESS_WORKBENCH__ = ${options.workbenchRuntime === true ? "true" : "false"};</script>
105
+ <script src="./assets/i18n.js?v=${assetVersion}"></script>
106
+ <script src="./assets/markdown-reader.js?v=${assetVersion}"></script>
107
+ <script src="./assets/mermaid-renderer.js?v=${assetVersion}"></script>
108
+ <script src="./assets/app.js?v=${assetVersion}"></script>
109
+ </body>
110
+ </html>`;
111
+ }
112
+
113
+ function readDashboardApp(templateRoot) {
114
+ const manifestPath = path.join(templateRoot, "assets/app.manifest.json");
115
+ if (!fs.existsSync(manifestPath)) return fs.readFileSync(path.join(templateRoot, "assets/app.js"), "utf8");
116
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
117
+ if (!Array.isArray(manifest) || manifest.length === 0) throw new Error(`Invalid dashboard app manifest: ${manifestPath}`);
118
+ return `${manifest.map((relativePath) => {
119
+ const source = path.join(templateRoot, "assets", relativePath);
120
+ if (!fs.existsSync(source)) throw new Error(`Dashboard app source missing: ${relativePath}`);
121
+ return fs.readFileSync(source, "utf8").trimEnd();
122
+ }).join("\n\n")}\n`;
123
+ }
124
+
45
125
  function assertSafeDashboardTarget(target, options) {
46
126
  const localizedDashboardTemplateRoot = dashboardTemplateRootForLocale(options.locale);
127
+ const sourceRepoRoot = options.repoRoot ? path.resolve(options.repoRoot) : "";
128
+ const projectRoot = options.projectRoot ? path.resolve(options.projectRoot) : "";
129
+ if (
130
+ sourceRepoRoot &&
131
+ projectRoot &&
132
+ isPathInside(projectRoot, sourceRepoRoot) &&
133
+ path.basename(projectRoot) === ".harness-private" &&
134
+ isPathInside(target, sourceRepoRoot) &&
135
+ !isPathInside(target, projectRoot)
136
+ ) {
137
+ throw new Error(`Refusing private dashboard output inside publishable source package: ${target}`);
138
+ }
47
139
  const protectedRoots = [
48
140
  path.parse(target).root,
49
141
  process.env.HOME,
@@ -71,8 +163,7 @@ function assertSafeDashboardTarget(target, options) {
71
163
  }
72
164
 
73
165
  function dashboardTemplateRootForLocale(locale = "en-US") {
74
- const localized = locale === "zh-CN" ? path.join(repoRoot, "templates-zh-CN/dashboard") : dashboardTemplateRoot;
75
- return fs.existsSync(localized) ? localized : dashboardTemplateRoot;
166
+ return dashboardTemplateRoot;
76
167
  }
77
168
 
78
169
  function isPathInside(candidate, parent) {