coding-agent-harness 1.0.1 → 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 (262) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.en-US.md +14 -0
  4. package/README.md +230 -80
  5. package/README.zh-CN.md +290 -0
  6. package/SKILL.md +132 -198
  7. package/docs-release/README.md +80 -9
  8. package/docs-release/architecture/overview.md +298 -28
  9. package/docs-release/architecture/overview.zh-CN.md +292 -0
  10. package/docs-release/assets/dashboard-overview.png +0 -0
  11. package/docs-release/assets/harness-architecture.svg +163 -0
  12. package/docs-release/assets/harness-workflow.svg +64 -0
  13. package/docs-release/guides/agent-installation.en-US.md +237 -0
  14. package/docs-release/guides/agent-installation.md +149 -27
  15. package/docs-release/guides/contributing.md +100 -0
  16. package/docs-release/guides/contributing.zh-CN.md +99 -0
  17. package/docs-release/guides/document-audience-and-surfaces.en-US.md +113 -0
  18. package/docs-release/guides/document-audience-and-surfaces.md +113 -0
  19. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
  20. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
  21. package/docs-release/guides/legacy-migration-agent-prompt.md +373 -0
  22. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +350 -0
  23. package/docs-release/guides/migration-playbook.en-US.md +324 -0
  24. package/docs-release/guides/migration-playbook.md +328 -0
  25. package/docs-release/guides/parent-control-repository-pattern.en-US.md +254 -0
  26. package/docs-release/guides/parent-control-repository-pattern.md +254 -0
  27. package/docs-release/guides/preset-development.md +214 -0
  28. package/docs-release/guides/repository-operating-models.en-US.md +197 -0
  29. package/docs-release/guides/repository-operating-models.md +197 -0
  30. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  31. package/docs-release/guides/task-state-machine.md +214 -0
  32. package/docs-release/intl/README.md +15 -0
  33. package/docs-release/intl/de-DE.md +18 -0
  34. package/docs-release/intl/en-US.md +18 -0
  35. package/docs-release/intl/es-ES.md +18 -0
  36. package/docs-release/intl/fr-FR.md +18 -0
  37. package/docs-release/intl/ja-JP.md +18 -0
  38. package/docs-release/intl/ko-KR.md +18 -0
  39. package/docs-release/intl/zh-CN.md +18 -0
  40. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
  41. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  42. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
  43. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
  44. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
  45. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
  46. package/package.json +10 -3
  47. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  48. package/presets/legacy-migration/preset.yaml +134 -0
  49. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  50. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  51. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  52. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  53. package/presets/legacy-migration/templates/review.seed.md +12 -0
  54. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  55. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  56. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  57. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  58. package/presets/lesson-sedimentation/preset.yaml +23 -0
  59. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  60. package/presets/module/preset.yaml +25 -0
  61. package/presets/module/templates/execution_strategy.append.md +8 -0
  62. package/presets/module/templates/task_plan.append.md +17 -0
  63. package/presets/standard-task/preset.yaml +31 -0
  64. package/presets/standard-task/templates/task_plan.append.md +7 -0
  65. package/references/adversarial-review-standard.md +2 -2
  66. package/references/agents-md-pattern.md +5 -5
  67. package/references/delivery-operating-model-standard.md +3 -3
  68. package/references/docs-directory-standard.md +53 -10
  69. package/references/external-source-intake-standard.md +75 -0
  70. package/references/harness-ledger.md +53 -94
  71. package/references/legacy-12-phase-bootstrap.md +41 -0
  72. package/references/lessons-governance.md +100 -88
  73. package/references/module-parallel-standard.md +14 -14
  74. package/references/planning-loop.md +51 -7
  75. package/references/project-onboarding-audit.md +10 -0
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +12 -1
  78. package/references/review-routing-standard.md +7 -1
  79. package/references/ssot-governance.md +67 -59
  80. package/references/taskr-gap-analysis.md +600 -0
  81. package/references/testing-standard.md +50 -0
  82. package/references/walkthrough-closeout.md +10 -9
  83. package/scripts/check-harness.mjs +111 -331
  84. package/scripts/commands/dashboard-command.mjs +67 -0
  85. package/scripts/commands/migration-command.mjs +96 -0
  86. package/scripts/commands/preset-command.mjs +73 -0
  87. package/scripts/commands/task-command.mjs +327 -0
  88. package/scripts/harness.mjs +106 -20
  89. package/scripts/lib/capability-registry.mjs +591 -0
  90. package/scripts/lib/check-module-parallel.mjs +237 -0
  91. package/scripts/lib/check-profiles.mjs +418 -0
  92. package/scripts/lib/check-task-contracts.mjs +47 -0
  93. package/scripts/lib/core-shared.mjs +196 -0
  94. package/scripts/lib/dashboard-data.mjs +412 -0
  95. package/scripts/lib/dashboard-workbench.mjs +257 -0
  96. package/scripts/lib/dashboard-writer.mjs +107 -4
  97. package/scripts/lib/git-status-summary.mjs +46 -0
  98. package/scripts/lib/governance-index-generator.mjs +174 -0
  99. package/scripts/lib/governance-sync.mjs +514 -0
  100. package/scripts/lib/governance-table-boundary.mjs +175 -0
  101. package/scripts/lib/harness-core.mjs +15 -1318
  102. package/scripts/lib/lesson-maintenance.mjs +152 -0
  103. package/scripts/lib/markdown-utils.mjs +158 -0
  104. package/scripts/lib/migration-planner.mjs +478 -0
  105. package/scripts/lib/migration-support.mjs +312 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +497 -0
  108. package/scripts/lib/preset-registry.mjs +627 -0
  109. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  110. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  111. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  112. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  113. package/scripts/lib/task-completion-consistency.mjs +16 -0
  114. package/scripts/lib/task-index.mjs +93 -0
  115. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  116. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  117. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  118. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  119. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  120. package/scripts/lib/task-lifecycle.mjs +649 -0
  121. package/scripts/lib/task-review-model.mjs +469 -0
  122. package/scripts/lib/task-scanner.mjs +576 -0
  123. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  124. package/scripts/postinstall.mjs +14 -0
  125. package/skills/preset-creator/SKILL.md +179 -0
  126. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  127. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  128. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  129. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  130. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  131. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  132. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  133. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  134. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  137. package/{templates/planning/visual_roadmap.md → skills/preset-creator/references/complex-task-skeleton/visual_map.md} +24 -2
  138. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  139. package/templates/AGENTS.md.template +51 -36
  140. package/templates/architecture/Architecture-SSoT.md +21 -0
  141. package/templates/architecture/README.md +49 -0
  142. package/templates/architecture/critical-flows.md +22 -0
  143. package/templates/architecture/local-repo-context.md +20 -0
  144. package/templates/architecture/service-catalog.md +17 -0
  145. package/templates/architecture/services/service-template.md +31 -0
  146. package/templates/architecture/system-map.md +22 -0
  147. package/templates/dashboard/assets/app-src/00-state.js +42 -0
  148. package/templates/dashboard/assets/app-src/10-router.js +77 -0
  149. package/templates/dashboard/assets/app-src/20-overview.js +241 -0
  150. package/templates/dashboard/assets/app-src/30-tasks.js +409 -0
  151. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  152. package/templates/dashboard/assets/app-src/40-modules.js +58 -0
  153. package/templates/dashboard/assets/app-src/45-review.js +347 -0
  154. package/templates/dashboard/assets/app-src/50-migration.js +183 -0
  155. package/templates/dashboard/assets/app-src/60-shared.js +61 -0
  156. package/templates/dashboard/assets/app-src/90-bindings.js +524 -0
  157. package/templates/dashboard/assets/app.css +3107 -300
  158. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  159. package/templates/dashboard/assets/app.js +2068 -306
  160. package/templates/dashboard/assets/app.manifest.json +12 -0
  161. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  162. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  163. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  164. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  165. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  166. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  167. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  168. package/templates/dashboard/assets/i18n.js +531 -44
  169. package/templates/dashboard/assets/mermaid-renderer.js +58 -8
  170. package/templates/development/README.md +52 -0
  171. package/templates/development/codebase-map.md +11 -0
  172. package/templates/development/cross-repo-debugging.md +18 -0
  173. package/templates/development/external-context/service-template.md +33 -0
  174. package/templates/development/external-source-packs/README.md +24 -0
  175. package/templates/development/external-source-packs/digest-template.md +28 -0
  176. package/templates/development/local-setup.md +16 -0
  177. package/templates/development/stubs-and-mocks.md +11 -0
  178. package/templates/integrations/README.md +40 -0
  179. package/templates/integrations/api-contract.md +42 -0
  180. package/templates/integrations/event-contract.md +46 -0
  181. package/templates/integrations/third-party/vendor-template.md +42 -0
  182. package/templates/integrations/webhook-contract.md +41 -0
  183. package/templates/ledger/Harness-Ledger.md +13 -25
  184. package/templates/lessons/lesson-arch-process-change.md +1 -1
  185. package/templates/lessons/lesson-new-doc.md +1 -1
  186. package/templates/lessons/lesson-ref-change.md +1 -1
  187. package/templates/planning/brief.md +32 -0
  188. package/templates/planning/execution_strategy.md +31 -0
  189. package/templates/planning/lesson_candidates.md +70 -0
  190. package/templates/planning/long-running-task-contract.md +7 -0
  191. package/templates/planning/module_brief.md +25 -0
  192. package/templates/planning/module_session_prompt.md +6 -0
  193. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  194. package/templates/planning/optional/references/INDEX.md +3 -3
  195. package/templates/planning/review.md +59 -0
  196. package/templates/planning/task_plan.md +40 -15
  197. package/templates/planning/visual_map.md +50 -0
  198. package/templates/reference/docs-library-standard.md +31 -0
  199. package/templates/reference/execution-workflow-standard.md +5 -2
  200. package/templates/reference/external-source-intake-standard.md +82 -0
  201. package/templates/reference/harness-ledger-standard.md +1 -0
  202. package/templates/reference/pull-request-standard.md +80 -0
  203. package/templates/reference/repo-governance-standard.md +8 -5
  204. package/templates/reference/review-routing-standard.md +6 -0
  205. package/templates/reference/walkthrough-standard.md +3 -1
  206. package/templates/verifier/verifier-output.md +1 -1
  207. package/templates/walkthrough/walkthrough-template.md +2 -2
  208. package/templates-zh-CN/AGENTS.md.template +73 -70
  209. package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
  210. package/templates-zh-CN/architecture/README.md +51 -0
  211. package/templates-zh-CN/architecture/critical-flows.md +24 -0
  212. package/templates-zh-CN/architecture/local-repo-context.md +20 -0
  213. package/templates-zh-CN/architecture/service-catalog.md +17 -0
  214. package/templates-zh-CN/architecture/services/service-template.md +31 -0
  215. package/templates-zh-CN/architecture/system-map.md +22 -0
  216. package/templates-zh-CN/development/README.md +54 -0
  217. package/templates-zh-CN/development/codebase-map.md +11 -0
  218. package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
  219. package/templates-zh-CN/development/external-context/service-template.md +33 -0
  220. package/templates-zh-CN/development/external-source-packs/README.md +24 -0
  221. package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
  222. package/templates-zh-CN/development/local-setup.md +16 -0
  223. package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
  224. package/templates-zh-CN/integrations/README.md +42 -0
  225. package/templates-zh-CN/integrations/api-contract.md +42 -0
  226. package/templates-zh-CN/integrations/event-contract.md +46 -0
  227. package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
  228. package/templates-zh-CN/integrations/webhook-contract.md +41 -0
  229. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  230. package/templates-zh-CN/planning/brief.md +32 -0
  231. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  232. package/templates-zh-CN/planning/lesson_candidates.md +70 -0
  233. package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
  234. package/templates-zh-CN/planning/module_brief.md +25 -0
  235. package/templates-zh-CN/planning/module_plan.md +2 -2
  236. package/templates-zh-CN/planning/module_session_prompt.md +4 -3
  237. package/templates-zh-CN/planning/review.md +59 -1
  238. package/templates-zh-CN/planning/task_plan.md +37 -11
  239. package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
  240. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  241. package/templates-zh-CN/reference/docs-library-standard.md +36 -1
  242. package/templates-zh-CN/reference/execution-workflow-standard.md +10 -2
  243. package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
  244. package/templates-zh-CN/reference/harness-ledger-standard.md +7 -4
  245. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  246. package/templates-zh-CN/reference/repo-governance-standard.md +4 -1
  247. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  248. package/templates-zh-CN/reference/walkthrough-standard.md +6 -5
  249. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
  250. package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
  251. package/scripts/smoke-dashboard.mjs +0 -70
  252. package/scripts/test-harness.mjs +0 -483
  253. package/templates/ssot/Feature-SSoT.md +0 -43
  254. package/templates/ssot/Lessons-SSoT.md +0 -44
  255. package/templates-zh-CN/dashboard/assets/app.css +0 -399
  256. package/templates-zh-CN/dashboard/assets/app.js +0 -435
  257. package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
  258. package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
  259. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
  260. package/templates-zh-CN/dashboard/index.html +0 -18
  261. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  262. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -0,0 +1,257 @@
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 { createLessonSedimentationTask } from "./task-lesson-sedimentation.mjs";
9
+ import { normalizeTarget } from "./core-shared.mjs";
10
+ import { collectTasks } from "./task-scanner.mjs";
11
+ import { writeDashboardFolder } from "./dashboard-data.mjs";
12
+
13
+ const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
14
+
15
+ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench", recoverGeneratedDashboard = false } = {}) {
16
+ if (host !== "127.0.0.1") throw new Error("dashboard workbench only supports --host 127.0.0.1");
17
+ const target = normalizeTarget(targetInput);
18
+ const outputDir = path.resolve(outDir);
19
+ const csrfToken = crypto.randomBytes(24).toString("hex");
20
+ const options = localeOverride ? { localeOverride } : {};
21
+ let snapshotVersion = Date.now();
22
+ const regenerate = () => {
23
+ writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true, recoverGeneratedDashboard });
24
+ snapshotVersion = Date.now();
25
+ };
26
+ regenerate();
27
+
28
+ const server = http.createServer(async (request, response) => {
29
+ try {
30
+ const address = server.address();
31
+ const actualPort = typeof address === "object" && address ? address.port : port;
32
+ const origin = `http://${host}:${actualPort}`;
33
+ const requestUrl = new URL(request.url || "/", origin);
34
+
35
+ if (requestUrl.pathname === "/api/runtime" && request.method === "GET") {
36
+ writeJson(response, 200, {
37
+ mode: "workbench",
38
+ csrfToken,
39
+ writableActions: ["review-complete", "lesson-sedimentation-task"],
40
+ target: target.projectRoot,
41
+ autoRefresh: autoRefresh === true,
42
+ snapshotVersion,
43
+ });
44
+ return;
45
+ }
46
+
47
+ if (requestUrl.pathname === "/api/tasks/review-complete" && request.method === "POST") {
48
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
49
+ const body = await readJsonBody(request);
50
+ const taskId = String(body.taskId || "");
51
+ const task = collectTasks(target).find((item) => item.id === taskId);
52
+ if (!task) {
53
+ writeJson(response, 404, { error: "Task not found" });
54
+ return;
55
+ }
56
+ if (!isTaskInReviewQueue(task)) {
57
+ writeJson(response, 409, reviewQueueRejectionPayload(task));
58
+ return;
59
+ }
60
+ if (task.reviewStatus === "confirmed") {
61
+ writeJson(response, 409, { error: "Review is already confirmed." });
62
+ return;
63
+ }
64
+ const result = confirmTaskReview(target.projectRoot, taskId, {
65
+ reviewer: body.reviewer || "Human Reviewer",
66
+ message: body.message || "confirmed from dashboard workbench",
67
+ evidence: body.evidence || "",
68
+ confirmText: body.confirmText || "",
69
+ });
70
+ regenerate();
71
+ writeJson(response, 200, result);
72
+ return;
73
+ }
74
+
75
+ if (requestUrl.pathname === "/api/tasks/lesson-sedimentation" && request.method === "POST") {
76
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
77
+ const body = await readJsonBody(request);
78
+ const taskId = String(body.taskId || "");
79
+ const candidateId = String(body.candidateId || "");
80
+ const task = collectTasks(target).find((item) => item.id === taskId);
81
+ if (!task) {
82
+ writeJson(response, 404, { error: "Task not found" });
83
+ return;
84
+ }
85
+ if (!candidateId) {
86
+ writeJson(response, 400, { error: "Missing lesson candidate id" });
87
+ return;
88
+ }
89
+ const result = createLessonSedimentationTask(target.projectRoot, taskId, candidateId, {
90
+ title: body.title || "",
91
+ });
92
+ regenerate();
93
+ writeJson(response, 200, result);
94
+ return;
95
+ }
96
+
97
+ if (request.method !== "GET" && request.method !== "HEAD") {
98
+ writeJson(response, 405, { error: "Method not allowed" });
99
+ return;
100
+ }
101
+ serveStaticFile(response, outputDir, requestUrl.pathname, request.method === "HEAD");
102
+ } catch (error) {
103
+ const status = error.status || (/CSRF|Origin|Host/.test(error.message) ? 403 : 400);
104
+ writeJson(response, status, errorPayload(error));
105
+ }
106
+ });
107
+
108
+ await new Promise((resolve, reject) => {
109
+ server.once("error", reject);
110
+ server.listen(Number(port), host, resolve);
111
+ });
112
+ const address = server.address();
113
+ const actualPort = typeof address === "object" && address ? address.port : port;
114
+ const url = `http://${host}:${actualPort}/`;
115
+ let watcher = null;
116
+ if (autoRefresh) watcher = startPollingWatch(target.docsRoot, regenerate);
117
+ console.log(`${label}: ${url} csrf=${csrfToken} outDir=${outputDir}`);
118
+ if (open) openBrowser(url);
119
+
120
+ const close = () => {
121
+ if (watcher) clearInterval(watcher);
122
+ server.close(() => process.exit(0));
123
+ };
124
+ process.once("SIGINT", close);
125
+ process.once("SIGTERM", close);
126
+ await new Promise(() => {});
127
+ }
128
+
129
+ function isTaskInReviewQueue(task) {
130
+ return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
131
+ }
132
+
133
+ function reviewQueueRejectionPayload(task) {
134
+ return {
135
+ error: "Review completion is only available for tasks in the review queue.",
136
+ reviewQueueState: task?.reviewQueueState || "unknown",
137
+ taskQueues: Array.isArray(task?.taskQueues) ? task.taskQueues : [],
138
+ queueReasons: Array.isArray(task?.queueReasons) ? task.queueReasons : [],
139
+ repairPrompt: task?.repairPrompt || "",
140
+ reviewStatus: task?.reviewStatus || "unknown",
141
+ taskId: task?.id || "",
142
+ };
143
+ }
144
+
145
+ function startPollingWatch(root, regenerate) {
146
+ let lastMtime = latestTreeMtime(root);
147
+ let timer = null;
148
+ return setInterval(() => {
149
+ const nextMtime = latestTreeMtime(root);
150
+ if (nextMtime <= lastMtime) return;
151
+ lastMtime = nextMtime;
152
+ clearTimeout(timer);
153
+ timer = setTimeout(() => {
154
+ try {
155
+ regenerate();
156
+ } catch (error) {
157
+ console.error(`dashboard regeneration failed: ${error.message}`);
158
+ }
159
+ }, 250);
160
+ }, 1000);
161
+ }
162
+
163
+ function latestTreeMtime(root) {
164
+ let latest = 0;
165
+ if (!fs.existsSync(root)) return latest;
166
+ const visit = (dir) => {
167
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
168
+ if ([".git", "node_modules", "tmp"].includes(entry.name)) continue;
169
+ const fullPath = path.join(dir, entry.name);
170
+ const stat = fs.statSync(fullPath);
171
+ latest = Math.max(latest, stat.mtimeMs);
172
+ if (entry.isDirectory()) visit(fullPath);
173
+ }
174
+ };
175
+ visit(root);
176
+ return latest;
177
+ }
178
+
179
+ function openBrowser(url) {
180
+ const command =
181
+ process.platform === "darwin" ? "open" :
182
+ process.platform === "win32" ? "cmd" :
183
+ "xdg-open";
184
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
185
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
186
+ child.on("error", () => {});
187
+ child.unref();
188
+ }
189
+
190
+ function assertTrustedWorkbenchRequest(request, { origin, csrfToken }) {
191
+ const host = request.headers.host || "";
192
+ if (host !== origin.replace(/^http:\/\//, "")) throw new Error("Host mismatch");
193
+ if (request.headers.origin !== origin) throw new Error("Origin mismatch");
194
+ if (request.headers["x-harness-csrf"] !== csrfToken) throw new Error("CSRF token mismatch");
195
+ }
196
+
197
+ function readJsonBody(request) {
198
+ return new Promise((resolve, reject) => {
199
+ let raw = "";
200
+ request.setEncoding("utf8");
201
+ request.on("data", (chunk) => {
202
+ raw += chunk;
203
+ if (raw.length > 32_768) {
204
+ reject(new Error("Request body too large"));
205
+ request.destroy();
206
+ }
207
+ });
208
+ request.on("end", () => {
209
+ try {
210
+ resolve(raw ? JSON.parse(raw) : {});
211
+ } catch {
212
+ reject(new Error("Invalid JSON body"));
213
+ }
214
+ });
215
+ request.on("error", reject);
216
+ });
217
+ }
218
+
219
+ function serveStaticFile(response, outputDir, urlPath, headOnly) {
220
+ const decoded = decodeURIComponent(urlPath);
221
+ const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
222
+ const filePath = path.resolve(outputDir, relative);
223
+ if (!isPathInside(filePath, outputDir) || !fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
224
+ writeJson(response, 404, { error: "Not found" });
225
+ return;
226
+ }
227
+ response.writeHead(200, { "content-type": mimeType(filePath), "cache-control": "no-store" });
228
+ if (!headOnly) response.end(fs.readFileSync(filePath));
229
+ else response.end();
230
+ }
231
+
232
+ function writeJson(response, status, payload) {
233
+ response.writeHead(status, jsonHeaders);
234
+ response.end(`${JSON.stringify(payload)}\n`);
235
+ }
236
+
237
+ function errorPayload(error) {
238
+ const payload = { error: error.message };
239
+ if (error.code) payload.code = error.code;
240
+ if (Array.isArray(error.recovery) && error.recovery.length > 0) payload.recovery = error.recovery;
241
+ if (error.details) payload.details = error.details;
242
+ return payload;
243
+ }
244
+
245
+ function mimeType(filePath) {
246
+ if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
247
+ if (filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
248
+ if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
249
+ if (filePath.endsWith(".json")) return "application/json; charset=utf-8";
250
+ if (filePath.endsWith(".md")) return "text/markdown; charset=utf-8";
251
+ return "application/octet-stream";
252
+ }
253
+
254
+ function isPathInside(candidate, parent) {
255
+ const relative = path.relative(path.resolve(parent), path.resolve(candidate));
256
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
257
+ }
@@ -18,8 +18,11 @@ export function writeDashboardDirectory(outDir, bundle, options = {}) {
18
18
  const target = path.resolve(outDir);
19
19
  assertSafeDashboardTarget(target, options);
20
20
  if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
21
- copyDashboardAssets(target, options);
21
+ fs.mkdirSync(target, { recursive: true });
22
22
  fs.writeFileSync(path.join(target, dashboardMarker), "generated dashboard directory\n");
23
+ copyDashboardAssets(target, options);
24
+ fs.writeFileSync(path.join(target, "assets/app.js"), readDashboardApp(dashboardTemplateRootForLocale(options.locale)));
25
+ fs.writeFileSync(path.join(target, "index.html"), renderDashboardIndex(options.locale, options));
23
26
  writeJsonFile(path.join(target, "data/status.json"), bundle.status);
24
27
  writeJsonFile(path.join(target, "data/tables.json"), bundle.tables);
25
28
  writeJsonFile(path.join(target, "data/documents.json"), bundle.documents);
@@ -42,8 +45,98 @@ export function writeDashboardDirectory(outDir, bundle, options = {}) {
42
45
  return target;
43
46
  }
44
47
 
48
+ export function writeDashboardFile(outFile, bundle, options = {}) {
49
+ const target = path.resolve(outFile);
50
+ fs.mkdirSync(path.dirname(target), { recursive: true });
51
+ fs.writeFileSync(target, renderDashboardFile(bundle, options));
52
+ return target;
53
+ }
54
+
55
+ export function renderDashboardFile(bundle, options = {}) {
56
+ const templateRoot = dashboardTemplateRootForLocale(options.locale);
57
+ const readAsset = (relativePath) => fs.readFileSync(path.join(templateRoot, relativePath), "utf8");
58
+ const css = readAsset("assets/app.css");
59
+ const i18n = readAsset("assets/i18n.js");
60
+ const markdown = readAsset("assets/markdown-reader.js");
61
+ const mermaid = readAsset("assets/mermaid-renderer.js");
62
+ const app = readDashboardApp(templateRoot);
63
+ const title = options.locale === "zh-CN" ? "Harness 控制台" : "Harness Dashboard";
64
+ const payload = JSON.stringify(bundle).replace(/</g, "\\u003c");
65
+ const runtimeLocale = options.locale === "zh-CN" ? "zh" : "en";
66
+ return `<!doctype html>
67
+ <html lang="${options.locale === "zh-CN" ? "zh-CN" : "en"}">
68
+ <head>
69
+ <meta charset="utf-8">
70
+ <meta name="viewport" content="width=device-width, initial-scale=1">
71
+ <title>${title}</title>
72
+ <link rel="icon" href="data:,">
73
+ <style>${css}</style>
74
+ </head>
75
+ <body>
76
+ <div id="app" class="app-shell" aria-live="polite"></div>
77
+ <script>window.__HARNESS_DASHBOARD__ = ${payload};</script>
78
+ <script>window.__HARNESS_LOCALE__ = ${JSON.stringify(runtimeLocale)};</script>
79
+ <script>${i18n}</script>
80
+ <script>${markdown}</script>
81
+ <script>${mermaid}</script>
82
+ <script>${app}</script>
83
+ </body>
84
+ </html>`;
85
+ }
86
+
87
+ function renderDashboardIndex(locale = "en-US", options = {}) {
88
+ const normalizedLocale = locale === "zh-CN" ? "zh-CN" : "en";
89
+ const runtimeLocale = locale === "zh-CN" ? "zh" : "en";
90
+ const title = locale === "zh-CN" ? "Harness 控制台" : "Harness Dashboard";
91
+ const assetVersion = Date.now();
92
+ return `<!doctype html>
93
+ <html lang="${normalizedLocale}">
94
+ <head>
95
+ <meta charset="utf-8">
96
+ <meta name="viewport" content="width=device-width, initial-scale=1">
97
+ <title>${title}</title>
98
+ <link rel="icon" href="data:,">
99
+ <link rel="stylesheet" href="./assets/app.css?v=${assetVersion}">
100
+ </head>
101
+ <body>
102
+ <div id="app" class="app-shell" aria-live="polite"></div>
103
+ <script src="./assets/dashboard-data.js?v=${assetVersion}"></script>
104
+ <script>window.__HARNESS_LOCALE__ = ${JSON.stringify(runtimeLocale)};</script>
105
+ <script>window.__HARNESS_WORKBENCH__ = ${options.workbenchRuntime === true ? "true" : "false"};</script>
106
+ <script src="./assets/i18n.js?v=${assetVersion}"></script>
107
+ <script src="./assets/markdown-reader.js?v=${assetVersion}"></script>
108
+ <script src="./assets/mermaid-renderer.js?v=${assetVersion}"></script>
109
+ <script src="./assets/app.js?v=${assetVersion}"></script>
110
+ </body>
111
+ </html>`;
112
+ }
113
+
114
+ function readDashboardApp(templateRoot) {
115
+ const manifestPath = path.join(templateRoot, "assets/app.manifest.json");
116
+ if (!fs.existsSync(manifestPath)) return fs.readFileSync(path.join(templateRoot, "assets/app.js"), "utf8");
117
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
118
+ if (!Array.isArray(manifest) || manifest.length === 0) throw new Error(`Invalid dashboard app manifest: ${manifestPath}`);
119
+ return `${manifest.map((relativePath) => {
120
+ const source = path.join(templateRoot, "assets", relativePath);
121
+ if (!fs.existsSync(source)) throw new Error(`Dashboard app source missing: ${relativePath}`);
122
+ return fs.readFileSync(source, "utf8").trimEnd();
123
+ }).join("\n\n")}\n`;
124
+ }
125
+
45
126
  function assertSafeDashboardTarget(target, options) {
46
127
  const localizedDashboardTemplateRoot = dashboardTemplateRootForLocale(options.locale);
128
+ const sourceRepoRoot = options.repoRoot ? path.resolve(options.repoRoot) : "";
129
+ const projectRoot = options.projectRoot ? path.resolve(options.projectRoot) : "";
130
+ if (
131
+ sourceRepoRoot &&
132
+ projectRoot &&
133
+ isPathInside(projectRoot, sourceRepoRoot) &&
134
+ path.basename(projectRoot) === ".harness-private" &&
135
+ isPathInside(target, sourceRepoRoot) &&
136
+ !isPathInside(target, projectRoot)
137
+ ) {
138
+ throw new Error(`Refusing private dashboard output inside publishable source package: ${target}`);
139
+ }
47
140
  const protectedRoots = [
48
141
  path.parse(target).root,
49
142
  process.env.HOME,
@@ -64,15 +157,25 @@ function assertSafeDashboardTarget(target, options) {
64
157
  if (fs.existsSync(target)) {
65
158
  const entries = fs.readdirSync(target);
66
159
  const hasMarker = fs.existsSync(path.join(target, dashboardMarker));
67
- if (entries.length > 0 && !hasMarker) {
160
+ const canRecoverGeneratedDashboard = options.recoverGeneratedDashboard === true && looksLikeGeneratedDashboardDirectory(target);
161
+ if (entries.length > 0 && !hasMarker && !canRecoverGeneratedDashboard) {
68
162
  throw new Error(`Refusing to overwrite non-dashboard directory: ${target}`);
69
163
  }
70
164
  }
71
165
  }
72
166
 
167
+ function looksLikeGeneratedDashboardDirectory(target) {
168
+ return [
169
+ "index.html",
170
+ "README.md",
171
+ "assets/app.js",
172
+ "assets/dashboard-data.js",
173
+ "data/status.json",
174
+ ].every((relativePath) => fs.existsSync(path.join(target, relativePath)));
175
+ }
176
+
73
177
  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;
178
+ return dashboardTemplateRoot;
76
179
  }
77
180
 
78
181
  function isPathInside(candidate, parent) {
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { inspectGit } from "./governance-sync.mjs";
4
+
5
+ export function summarizeGitState(target) {
6
+ const state = inspectGit(target.projectRoot);
7
+ if (!state.inGit) {
8
+ return {
9
+ summary: {
10
+ inGit: false,
11
+ root: "",
12
+ dirty: false,
13
+ entries: [],
14
+ blocksCliAutoCommit: false,
15
+ },
16
+ warnings: [],
17
+ };
18
+ }
19
+ const targetRoot = real(target.projectRoot);
20
+ const gitRoot = real(state.gitRoot);
21
+ const rootMatches = gitRoot === targetRoot;
22
+ const entries = rootMatches ? state.entries.map((entry) => ({
23
+ path: entry.path,
24
+ index: entry.index,
25
+ worktree: entry.worktree,
26
+ })) : [];
27
+ const dirty = entries.length > 0;
28
+ const warnings = [];
29
+ if (dirty) {
30
+ warnings.push(`dirty-state: ${entries.length} uncommitted Git path(s) will block CLI-owned auto-commit; commit them or record owner/no-commit reason before lifecycle commands.`);
31
+ }
32
+ return {
33
+ summary: {
34
+ inGit: true,
35
+ root: rootMatches ? "TARGET:." : "outside-target",
36
+ dirty,
37
+ entries,
38
+ blocksCliAutoCommit: !rootMatches || dirty,
39
+ },
40
+ warnings,
41
+ };
42
+ }
43
+
44
+ function real(filePath) {
45
+ return fs.realpathSync(filePath);
46
+ }
@@ -0,0 +1,174 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ normalizeTarget,
5
+ readBundledTemplate,
6
+ todayDate,
7
+ toPosix,
8
+ } from "./core-shared.mjs";
9
+ import { splitMarkdownRow } from "./markdown-utils.mjs";
10
+ import { collectTasks } from "./task-scanner.mjs";
11
+ import {
12
+ beginGovernanceSync,
13
+ commitGovernanceSync,
14
+ moduleGeneratedIndexSurfaces,
15
+ releaseGovernanceSync,
16
+ } from "./governance-sync.mjs";
17
+ import { markdownCell } from "./task-lifecycle/text-utils.mjs";
18
+
19
+ export function rebuildGovernanceIndexes(targetInput, { dryRun = false, archive = false, apply = false } = {}) {
20
+ const target = normalizeTarget(targetInput);
21
+ const effectiveApply = Boolean(apply && !dryRun);
22
+ const context = beginGovernanceSync(target, { operation: "governance rebuild", dryRun: !effectiveApply });
23
+ try {
24
+ const tasks = collectTasks(target)
25
+ .filter((task) => task.deletionState !== "deleted")
26
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)));
27
+ const surfaces = [...governanceSurfaces(target, tasks), ...moduleGeneratedIndexSurfaces(target, tasks)];
28
+ const archiveDir = archive ? uniqueArchiveDir(target) : "";
29
+ const changes = surfaces.map((surface) => ({
30
+ surface: surface.surface,
31
+ destination: surface.relative,
32
+ action: surface.archiveOnly
33
+ ? apply ? "archive-legacy-governance-table" : "would-archive-legacy-governance-table"
34
+ : apply ? "rebuild-governance-index" : "would-rebuild-governance-index",
35
+ generatedRows: surface.rows.length,
36
+ archive: archive ? `${archiveDir}/${surface.relative}` : "",
37
+ }));
38
+
39
+ if (!effectiveApply) {
40
+ return { dryRun: true, archive, applied: false, archiveDir, changes };
41
+ }
42
+
43
+ const allowed = [];
44
+ for (const surface of surfaces) {
45
+ if (archive && fs.existsSync(surface.absolute)) {
46
+ const archivePath = path.join(target.projectRoot, archiveDir, surface.relative);
47
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true });
48
+ fs.copyFileSync(surface.absolute, archivePath);
49
+ allowed.push(toPosix(path.relative(target.projectRoot, archivePath)));
50
+ }
51
+ if (surface.archiveOnly) {
52
+ if (archive && fs.existsSync(surface.absolute)) {
53
+ fs.rmSync(surface.absolute);
54
+ allowed.push(surface.relative);
55
+ }
56
+ continue;
57
+ }
58
+ fs.mkdirSync(path.dirname(surface.absolute), { recursive: true });
59
+ fs.writeFileSync(surface.absolute, surface.content);
60
+ allowed.push(surface.relative);
61
+ }
62
+
63
+ const commit = commitGovernanceSync(context, allowed, { message: "chore(harness): rebuild generated governance indexes" });
64
+ return { dryRun: false, archive, applied: true, archiveDir, changes, commit };
65
+ } finally {
66
+ releaseGovernanceSync(context);
67
+ }
68
+ }
69
+
70
+ function governanceSurfaces(target, tasks) {
71
+ return [
72
+ {
73
+ surface: "harness-ledger",
74
+ absolute: path.join(target.docsRoot, "Harness-Ledger.md"),
75
+ relative: toPosix(path.relative(target.projectRoot, path.join(target.docsRoot, "Harness-Ledger.md"))),
76
+ rows: tasks.map(ledgerRow),
77
+ content: replaceTableRows(readBundledTemplate("templates/ledger/Harness-Ledger.md"), /^ID$/i, tasks.map(ledgerRow)),
78
+ },
79
+ ...legacyFeatureSurfaces(target),
80
+ ];
81
+ }
82
+
83
+ function legacyFeatureSurfaces(target) {
84
+ return [
85
+ ["legacy-feature-ssot", path.join(target.docsRoot, "09-PLANNING/Feature-SSoT.md")],
86
+ ["legacy-private-feature-ssot", path.join(target.docsRoot, "09-PLANNING/Private-Feature-SSoT.md")],
87
+ ].filter(([, absolute]) => fs.existsSync(absolute)).map(([surface, absolute]) => ({
88
+ surface,
89
+ absolute,
90
+ relative: toPosix(path.relative(target.projectRoot, absolute)),
91
+ rows: [],
92
+ content: "",
93
+ archiveOnly: true,
94
+ }));
95
+ }
96
+
97
+ function replaceTableRows(content, headerPattern, rows) {
98
+ const lines = String(content || "").split(/\r?\n/);
99
+ for (let index = 0; index < lines.length - 1; index += 1) {
100
+ if (!lines[index].trim().startsWith("|")) continue;
101
+ const header = splitMarkdownRow(lines[index]);
102
+ if (!header.some((cell) => headerPattern.test(cell))) continue;
103
+ const separator = splitMarkdownRow(lines[index + 1]);
104
+ if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
105
+ let end = index + 2;
106
+ while (end < lines.length && lines[end].trim().startsWith("|")) end += 1;
107
+ const fitted = rows.map((row) => fitRow(row, header.length));
108
+ lines.splice(index + 2, end - index - 2, ...fitted.map((row) => `| ${row.join(" | ")} |`));
109
+ return `${lines.join("\n").trimEnd()}\n`;
110
+ }
111
+ return `${String(content || "").trimEnd()}\n\n${rows.map((row) => `| ${row.join(" | ")} |`).join("\n")}\n`;
112
+ }
113
+
114
+ function fitRow(row, length) {
115
+ const next = row.map((cell) => markdownCell(cell));
116
+ while (next.length < length) next.push("");
117
+ return next.slice(0, length);
118
+ }
119
+
120
+ function ledgerRow(task) {
121
+ const plan = stripTarget(task.taskPlanPath || `${stripTarget(task.path)}/task_plan.md`);
122
+ const scope = task.module ? "module" : "task";
123
+ const moduleKey = task.module || "none";
124
+ return [
125
+ taskLedgerId(task),
126
+ scope,
127
+ moduleKey,
128
+ task.title || task.shortId || task.id,
129
+ mapLedgerState(task.state),
130
+ Array.isArray(task.taskQueues) && task.taskQueues.length ? task.taskQueues.join(",") : "none",
131
+ plan,
132
+ task.reviewStatus === "confirmed" ? stripTarget(task.reviewPath) : (task.reviewStatus || "pending"),
133
+ task.lessonCandidateDecisionComplete ? "checked" : (task.lessonCandidateStatus || "pending"),
134
+ task.walkthroughPath ? stripTarget(task.walkthroughPath) : (task.closeoutStatus || "pending"),
135
+ residual(task),
136
+ todayDate(),
137
+ ];
138
+ }
139
+
140
+ function residual(task) {
141
+ if (Array.isArray(task.stateConflicts) && task.stateConflicts.length) return `state-conflicts:${task.stateConflicts.length}`;
142
+ if (Array.isArray(task.materialIssues) && task.materialIssues.length) return `material-issues:${task.materialIssues.length}`;
143
+ return "none";
144
+ }
145
+
146
+ function taskLedgerId(task) {
147
+ return `HL-${taskSlug(task)}`;
148
+ }
149
+
150
+ function taskSlug(task) {
151
+ return String(task.shortId || task.id || "task").replace(/^TASKS\//, "").replace(/^MODULES\//, "").replace(/[^A-Za-z0-9-]+/g, "-").slice(0, 72);
152
+ }
153
+
154
+ function stripTarget(value) {
155
+ return String(value || "").replace(/^TARGET:/, "");
156
+ }
157
+
158
+ function mapLedgerState(state) {
159
+ if (state === "in_progress") return "active";
160
+ if (state === "review") return "review";
161
+ if (state === "done") return "closed";
162
+ if (state === "blocked") return "blocked";
163
+ return "planned";
164
+ }
165
+
166
+ function uniqueArchiveDir(target) {
167
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(".", "-");
168
+ const base = `docs/01-GOVERNANCE/archive/generated-governance-tables/${stamp}`;
169
+ for (let index = 0; index < 100; index += 1) {
170
+ const candidate = index === 0 ? base : `${base}-${String(index).padStart(2, "0")}`;
171
+ if (!fs.existsSync(path.join(target.projectRoot, candidate))) return candidate;
172
+ }
173
+ throw new Error("Unable to allocate a unique governance table archive directory");
174
+ }