coding-agent-harness 1.0.8 → 1.1.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 (232) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CONTRIBUTING.md +8 -4
  3. package/README.md +12 -2
  4. package/README.zh-CN.md +10 -2
  5. package/SKILL.md +14 -3
  6. package/dist/build-dist.mjs +19 -6
  7. package/dist/check-dist-observation.mjs +57 -29
  8. package/dist/check-harness.mjs +0 -1
  9. package/dist/check-import-graph.mjs +44 -27
  10. package/dist/check-lite-forbidden-surfaces.mjs +121 -0
  11. package/dist/check-no-ts-nocheck.mjs +7 -7
  12. package/dist/check-runtime-emit.mjs +10 -3
  13. package/dist/check-type-boundaries.mjs +51 -9
  14. package/dist/commands/dashboard-command.mjs +52 -14
  15. package/dist/commands/migration-command.mjs +18 -8
  16. package/dist/commands/module-command.mjs +142 -0
  17. package/dist/commands/preset-command.mjs +51 -12
  18. package/dist/commands/registry.mjs +483 -0
  19. package/dist/commands/task-command.mjs +109 -52
  20. package/dist/harness.mjs +6 -304
  21. package/dist/lib/capability-registry.mjs +229 -53
  22. package/dist/lib/check-module-parallel.mjs +1 -6
  23. package/dist/lib/check-profiles.mjs +39 -46
  24. package/dist/lib/check-task-contracts.mjs +6 -4
  25. package/dist/lib/command-registry.mjs +248 -0
  26. package/dist/lib/core-shared.mjs +78 -3
  27. package/dist/lib/dashboard-data.mjs +203 -22
  28. package/dist/lib/dashboard-workbench.mjs +245 -21
  29. package/dist/lib/dashboard-writer.mjs +4 -1
  30. package/dist/lib/git-status-summary.mjs +0 -1
  31. package/dist/lib/governance-index-generator.mjs +7 -5
  32. package/dist/lib/governance-sync.mjs +46 -121
  33. package/dist/lib/governance-table-boundary.mjs +1 -14
  34. package/dist/lib/harness-core.mjs +4 -1
  35. package/dist/lib/harness-paths.mjs +115 -1
  36. package/dist/lib/impact-classifier.mjs +420 -0
  37. package/dist/lib/lesson-maintenance.mjs +1 -2
  38. package/dist/lib/markdown-utils.mjs +50 -1
  39. package/dist/lib/migration-planner.mjs +31 -16
  40. package/dist/lib/migration-support.mjs +5 -4
  41. package/dist/lib/module-registry.mjs +296 -0
  42. package/dist/lib/preset-audit-contracts.mjs +24 -1
  43. package/dist/lib/preset-engine.mjs +67 -29
  44. package/dist/lib/preset-registry.mjs +361 -71
  45. package/dist/lib/preset-runner.mjs +292 -26
  46. package/dist/lib/review-confirm-git-gate.mjs +73 -19
  47. package/dist/lib/status-builder.mjs +23 -8
  48. package/dist/lib/structure-migration.mjs +6 -4
  49. package/dist/lib/subagent-authorization-audit.mjs +8 -2
  50. package/dist/lib/task-archive-eligibility.mjs +65 -0
  51. package/dist/lib/task-audit-metadata.mjs +25 -11
  52. package/dist/lib/task-audit-migration.mjs +21 -14
  53. package/dist/lib/task-discovery-contract.mjs +32 -0
  54. package/dist/lib/task-index.mjs +3 -2
  55. package/dist/lib/task-lesson-candidates.mjs +1 -2
  56. package/dist/lib/task-lesson-sedimentation.mjs +310 -9
  57. package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
  58. package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
  59. package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
  60. package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
  61. package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
  62. package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
  63. package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
  64. package/dist/lib/task-lifecycle/template-files.mjs +2 -5
  65. package/dist/lib/task-lifecycle.mjs +116 -160
  66. package/dist/lib/task-metadata.mjs +10 -5
  67. package/dist/lib/task-preset-contract-drift.mjs +45 -0
  68. package/dist/lib/task-repository.mjs +192 -0
  69. package/dist/lib/task-review-model.mjs +36 -17
  70. package/dist/lib/task-scanner.mjs +74 -23
  71. package/dist/lib/task-template-materials.mjs +131 -0
  72. package/dist/lib/task-tombstone-commands.mjs +186 -29
  73. package/dist/lib/types/check-profiles.js +1 -0
  74. package/dist/lib/types/impact.js +1 -0
  75. package/dist/lib/types/preset.js +1 -0
  76. package/dist/lib/types/task-lifecycle.js +1 -0
  77. package/dist/lib/types/task-scanner.js +1 -0
  78. package/dist/postinstall.mjs +2 -2
  79. package/dist/run-built-tests.mjs +10 -3
  80. package/docs-release/README.md +1 -0
  81. package/docs-release/architecture/document-contract-kernel/README.md +150 -0
  82. package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
  83. package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
  84. package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
  85. package/docs-release/architecture/overview.md +2 -2
  86. package/docs-release/architecture/overview.zh-CN.md +2 -2
  87. package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
  88. package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
  89. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
  90. package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
  91. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
  92. package/docs-release/architecture/system-explainer/README.md +1 -1
  93. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
  94. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
  95. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
  96. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
  97. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
  98. package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
  99. package/docs-release/guides/agent-installation.en-US.md +4 -6
  100. package/docs-release/guides/agent-installation.md +11 -8
  101. package/docs-release/guides/contributing.md +10 -3
  102. package/docs-release/guides/contributing.zh-CN.md +10 -3
  103. package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
  104. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
  105. package/docs-release/guides/migration-playbook.en-US.md +9 -6
  106. package/docs-release/guides/migration-playbook.md +9 -6
  107. package/docs-release/guides/preset-development.md +68 -2
  108. package/docs-release/guides/task-state-machine.en-US.md +8 -8
  109. package/docs-release/guides/task-state-machine.md +7 -7
  110. package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
  111. package/package.json +16 -12
  112. package/postinstall.mjs +37 -0
  113. package/presets/legacy-migration/preset.yaml +5 -5
  114. package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
  115. package/presets/lesson-sedimentation/preset.yaml +3 -3
  116. package/presets/module/preset.yaml +2 -2
  117. package/presets/module/templates/execution_strategy.append.md +1 -1
  118. package/presets/module/templates/task_plan.append.md +3 -3
  119. package/presets/release-closeout/checks/check-release-package.mjs +6 -1
  120. package/presets/release-closeout/preset.yaml +9 -9
  121. package/presets/release-closeout/scripts/generate-release-package.mjs +387 -25
  122. package/presets/release-closeout/templates/task_plan.append.md +5 -5
  123. package/presets/standard-task/preset.yaml +2 -2
  124. package/references/agents-md-pattern.md +23 -17
  125. package/references/lessons-governance.md +2 -2
  126. package/references/module-parallel-standard.md +3 -6
  127. package/references/ssot-governance.md +2 -2
  128. package/references/taskr-gap-analysis.md +3 -3
  129. package/run-dist.mjs +34 -0
  130. package/skills/preset-creator/SKILL.md +40 -8
  131. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
  132. package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
  133. package/skills/preset-creator/references/structure-aware-paths.md +112 -0
  134. package/templates/AGENTS.md.template +28 -26
  135. package/templates/architecture/README.md +2 -2
  136. package/templates/architecture/service-catalog.md +2 -2
  137. package/templates/architecture/services/service-template.md +1 -1
  138. package/templates/dashboard/assets/app-src/00-state.js +5 -1
  139. package/templates/dashboard/assets/app-src/10-router.js +7 -0
  140. package/templates/dashboard/assets/app-src/20-overview.js +8 -8
  141. package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
  142. package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
  143. package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
  144. package/templates/dashboard/assets/app-src/40-modules.js +257 -41
  145. package/templates/dashboard/assets/app-src/45-review.js +127 -1
  146. package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
  147. package/templates/dashboard/assets/app.css +928 -53
  148. package/templates/dashboard/assets/app.css.manifest.json +2 -0
  149. package/templates/dashboard/assets/app.js +1071 -98
  150. package/templates/dashboard/assets/app.manifest.json +1 -0
  151. package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
  152. package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
  153. package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
  154. package/templates/dashboard/assets/css-src/31-archive.css +94 -0
  155. package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
  156. package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
  157. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
  158. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
  159. package/templates/dashboard/assets/i18n.js +166 -2
  160. package/templates/development/README.md +9 -9
  161. package/templates/development/cross-repo-debugging.md +3 -3
  162. package/templates/development/external-context/service-template.md +1 -1
  163. package/templates/development/external-source-packs/README.md +2 -2
  164. package/templates/integrations/README.md +4 -4
  165. package/templates/integrations/api-contract.md +1 -1
  166. package/templates/integrations/event-contract.md +1 -1
  167. package/templates/integrations/third-party/vendor-template.md +1 -1
  168. package/templates/integrations/webhook-contract.md +1 -1
  169. package/templates/ledger/Harness-Ledger.md +1 -1
  170. package/templates/modules/module_brief.md +50 -0
  171. package/templates/modules/module_plan.md +49 -0
  172. package/templates/modules/registry_view.md +9 -0
  173. package/templates/modules/session_prompt_pack.md +55 -0
  174. package/templates/planning/brief.md +32 -8
  175. package/templates/planning/module_brief.md +28 -3
  176. package/templates/planning/module_plan.md +26 -11
  177. package/templates/planning/module_session_prompt.md +11 -2
  178. package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
  179. package/templates/planning/review.md +1 -1
  180. package/templates/planning/visual_map.md +1 -1
  181. package/templates/reference/docs-library-standard.md +7 -7
  182. package/templates/reference/execution-workflow-standard.md +13 -0
  183. package/templates/reference/external-source-intake-standard.md +10 -10
  184. package/templates/reference/repo-governance-standard.md +1 -1
  185. package/templates/reference/review-routing-standard.md +4 -0
  186. package/templates/ssot/Module-Registry.md +4 -38
  187. package/templates/walkthrough/walkthrough-template.md +1 -1
  188. package/templates-zh-CN/AGENTS.md.template +27 -25
  189. package/templates-zh-CN/CLAUDE.md.template +1 -1
  190. package/templates-zh-CN/architecture/README.md +2 -2
  191. package/templates-zh-CN/architecture/service-catalog.md +2 -2
  192. package/templates-zh-CN/architecture/services/service-template.md +1 -1
  193. package/templates-zh-CN/development/README.md +9 -9
  194. package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
  195. package/templates-zh-CN/development/external-context/service-template.md +1 -1
  196. package/templates-zh-CN/development/external-source-packs/README.md +2 -2
  197. package/templates-zh-CN/integrations/README.md +4 -4
  198. package/templates-zh-CN/integrations/api-contract.md +1 -1
  199. package/templates-zh-CN/integrations/event-contract.md +1 -1
  200. package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
  201. package/templates-zh-CN/integrations/webhook-contract.md +1 -1
  202. package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
  203. package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
  204. package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
  205. package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
  206. package/templates-zh-CN/modules/module_brief.md +47 -0
  207. package/templates-zh-CN/modules/module_plan.md +48 -0
  208. package/templates-zh-CN/modules/registry_view.md +9 -0
  209. package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
  210. package/templates-zh-CN/planning/INDEX.md +1 -0
  211. package/templates-zh-CN/planning/brief.md +26 -7
  212. package/templates-zh-CN/planning/module_brief.md +24 -2
  213. package/templates-zh-CN/planning/module_plan.md +35 -29
  214. package/templates-zh-CN/planning/module_session_prompt.md +15 -11
  215. package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
  216. package/templates-zh-CN/planning/review.md +1 -1
  217. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  218. package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
  219. package/templates-zh-CN/reference/docs-library-standard.md +27 -27
  220. package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
  221. package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
  222. package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
  223. package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
  224. package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
  225. package/templates-zh-CN/reference/review-routing-standard.md +3 -0
  226. package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
  227. package/templates-zh-CN/reference/worktree-standard.md +1 -1
  228. package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
  229. package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
  230. package/templates-zh-CN/ssot/Module-Registry.md +5 -44
  231. package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
  232. package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  // Dashboard workbench HTTP handlers stay behavior-first until workbench request/response types are modeled.
3
2
  import crypto from "node:crypto";
4
3
  import { spawn } from "node:child_process";
@@ -6,24 +5,26 @@ import fs from "node:fs";
6
5
  import http from "node:http";
7
6
  import path from "node:path";
8
7
  import { URL } from "node:url";
9
- import { confirmTaskReview } from "./task-lifecycle.mjs";
10
- import { createLessonSedimentationTask } from "./task-lesson-sedimentation.mjs";
8
+ import { confirmTaskReview, finalizeDeferredTaskReviewConfirmation, updateTaskLifecycle } from "./task-lifecycle.mjs";
9
+ import { createAggregateLessonSedimentationTask, createLessonSedimentationTask } from "./task-lesson-sedimentation.mjs";
11
10
  import { normalizeTarget } from "./core-shared.mjs";
11
+ import { beginGovernanceSync, commitGovernanceSync, releaseGovernanceSync } from "./governance-sync.mjs";
12
12
  import { dashboardWatchRoots } from "./harness-paths.mjs";
13
- import { collectTasks } from "./task-scanner.mjs";
13
+ import { createScannerTaskRepository } from "./task-repository.mjs";
14
14
  import { writeDashboardFolder } from "./dashboard-data.mjs";
15
15
  import { checkPresetPackage, installPresetPackage, listPresetPackages, seedBundledPresets, uninstallPresetPackage, } from "./preset-registry.mjs";
16
- const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
17
- export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench", recoverGeneratedDashboard = false } = {}) {
16
+ const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "connection": "close" };
17
+ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench", recoverGeneratedDashboard = false, replaceExistingDashboardOutput = false } = {}) {
18
18
  if (host !== "127.0.0.1")
19
19
  throw new Error("dashboard workbench only supports --host 127.0.0.1");
20
20
  const target = normalizeTarget(targetInput);
21
+ const taskRepository = createScannerTaskRepository(target);
21
22
  const outputDir = path.resolve(outDir);
22
23
  const csrfToken = crypto.randomBytes(24).toString("hex");
23
24
  const options = localeOverride ? { localeOverride } : {};
24
25
  let snapshotVersion = Date.now();
25
26
  const regenerate = () => {
26
- writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true, recoverGeneratedDashboard });
27
+ writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true, recoverGeneratedDashboard, replaceExistingDashboardOutput });
27
28
  snapshotVersion = Date.now();
28
29
  };
29
30
  regenerate();
@@ -37,7 +38,7 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
37
38
  writeJson(response, 200, {
38
39
  mode: "workbench",
39
40
  csrfToken,
40
- writableActions: ["review-complete", "lesson-sedimentation-task", "preset-check", "preset-install", "preset-seed", "preset-uninstall"],
41
+ writableActions: ["review-complete", "review-complete-bulk", "task-complete", "lesson-sedimentation-task", "lesson-sedimentation-bulk", "preset-check", "preset-install", "preset-seed", "preset-uninstall"],
41
42
  target: target.projectRoot,
42
43
  autoRefresh: autoRefresh === true,
43
44
  snapshotVersion,
@@ -48,7 +49,7 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
48
49
  assertTrustedWorkbenchRequest(request, { origin, csrfToken });
49
50
  const body = await readJsonBody(request);
50
51
  const taskId = String(body.taskId || "");
51
- const task = collectTasks(target).find((item) => item.id === taskId);
52
+ const task = findWorkbenchTask(taskRepository, taskId);
52
53
  if (!task) {
53
54
  writeJson(response, 404, { error: "Task not found" });
54
55
  return;
@@ -71,12 +72,100 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
71
72
  writeJson(response, 200, result);
72
73
  return;
73
74
  }
75
+ if (requestUrl.pathname === "/api/tasks/task-complete" && request.method === "POST") {
76
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
77
+ const body = await readJsonBody(request);
78
+ const taskId = String(body.taskId || "");
79
+ const task = findWorkbenchTask(taskRepository, taskId);
80
+ if (!task) {
81
+ writeJson(response, 404, { error: "Task not found" });
82
+ return;
83
+ }
84
+ if (taskHasPendingLessonWork(task)) {
85
+ writeJson(response, 409, closeoutRejectionPayload(task, "Lesson candidate promotion or sedimentation work must be resolved before closeout."));
86
+ return;
87
+ }
88
+ if (!taskReadyForCloseout(task)) {
89
+ writeJson(response, 409, closeoutRejectionPayload(task));
90
+ return;
91
+ }
92
+ const result = updateTaskLifecycle(target.projectRoot, taskId, {
93
+ event: "task-complete",
94
+ state: "done",
95
+ message: body.message || "closed from dashboard workbench",
96
+ evidence: body.evidence || "",
97
+ });
98
+ regenerate();
99
+ writeJson(response, 200, result);
100
+ return;
101
+ }
102
+ if (requestUrl.pathname === "/api/tasks/review-complete-bulk" && request.method === "POST") {
103
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
104
+ const body = await readJsonBody(request);
105
+ const requestedTaskIds = Array.isArray(body.taskIds) ? body.taskIds.map((id) => String(id || "").trim()).filter(Boolean) : [];
106
+ const taskIds = uniqueValues(requestedTaskIds);
107
+ if (taskIds.length === 0) {
108
+ writeJson(response, 400, { error: "No review tasks selected" });
109
+ return;
110
+ }
111
+ const results = [];
112
+ for (const taskId of taskIds) {
113
+ const task = findWorkbenchTask(taskRepository, taskId);
114
+ if (!task) {
115
+ results.push({ taskId, ok: false, status: 404, error: "Task not found" });
116
+ continue;
117
+ }
118
+ if (!isTaskInReviewQueue(task)) {
119
+ const payload = reviewQueueRejectionPayload(task);
120
+ results.push({ taskId, ok: false, status: 409, ...payload });
121
+ continue;
122
+ }
123
+ if (task.reviewStatus === "confirmed") {
124
+ results.push({ taskId, ok: false, status: 409, error: "Review is already confirmed." });
125
+ continue;
126
+ }
127
+ try {
128
+ const result = confirmTaskReview(target.projectRoot, taskId, {
129
+ reviewer: body.reviewer || "Human Reviewer",
130
+ message: body.message || "bulk confirmed from dashboard workbench",
131
+ evidence: body.evidence || "",
132
+ confirmText: task.shortId || task.id,
133
+ deferCommit: true,
134
+ });
135
+ results.push({ taskId, ok: true, status: 200, audit: result.audit, task: { id: result.task?.id || taskId } });
136
+ }
137
+ catch (error) {
138
+ results.push({ taskId, ok: false, status: errorStatus(error), ...errorPayload(error) });
139
+ }
140
+ }
141
+ const confirmed = results.filter((result) => result.ok).length;
142
+ const failed = results.length - confirmed;
143
+ if (confirmed > 0) {
144
+ const allowedPaths = uniqueValues(results.filter((result) => result.ok).flatMap((result) => result.audit?.allowedPaths || []));
145
+ const confirmCommit = commitWorkbenchBatch(target, allowedPaths, { operation: "review-complete-bulk", message: "chore: confirm selected reviews" });
146
+ for (const result of results.filter((item) => item.ok)) {
147
+ finalizeDeferredTaskReviewConfirmation(target.projectRoot, result.taskId, { commitSha: confirmCommit.commitSha });
148
+ }
149
+ const auditCommit = commitWorkbenchBatch(target, allowedPaths, { operation: "review-complete-bulk-audit", message: "chore: record selected review confirmation audit" });
150
+ for (const result of results.filter((item) => item.ok)) {
151
+ result.audit = {
152
+ ...result.audit,
153
+ commitSha: confirmCommit.commitSha,
154
+ auditCommitSha: auditCommit.commitSha,
155
+ auditStatus: "committed",
156
+ allowedPaths,
157
+ };
158
+ }
159
+ }
160
+ writeJson(response, confirmed > 0 ? 200 : 409, { ok: failed === 0, confirmed, failed, results });
161
+ return;
162
+ }
74
163
  if (requestUrl.pathname === "/api/tasks/lesson-sedimentation" && request.method === "POST") {
75
164
  assertTrustedWorkbenchRequest(request, { origin, csrfToken });
76
165
  const body = await readJsonBody(request);
77
166
  const taskId = String(body.taskId || "");
78
167
  const candidateId = String(body.candidateId || "");
79
- const task = collectTasks(target).find((item) => item.id === taskId);
168
+ const task = findWorkbenchTask(taskRepository, taskId);
80
169
  if (!task) {
81
170
  writeJson(response, 404, { error: "Task not found" });
82
171
  return;
@@ -92,6 +181,40 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
92
181
  writeJson(response, 200, result);
93
182
  return;
94
183
  }
184
+ if (requestUrl.pathname === "/api/tasks/lesson-sedimentation-bulk" && request.method === "POST") {
185
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
186
+ const body = await readJsonBody(request);
187
+ const selections = normalizeBulkLessonSelections(body.selections);
188
+ if (selections.length === 0) {
189
+ writeJson(response, 400, { error: "No lesson candidates selected" });
190
+ return;
191
+ }
192
+ try {
193
+ const result = createAggregateLessonSedimentationTask(target.projectRoot, selections, {
194
+ title: body.title || "",
195
+ });
196
+ const results = selections.map((selection) => ({
197
+ ...selection,
198
+ ok: true,
199
+ status: 200,
200
+ followUpTask: result.followUpTask,
201
+ }));
202
+ writeJson(response, 200, {
203
+ ok: true,
204
+ created: 1,
205
+ candidates: result.candidates.length,
206
+ failed: 0,
207
+ followUpTask: result.followUpTask,
208
+ prompt: result.prompt,
209
+ governance: result.governance,
210
+ results,
211
+ });
212
+ }
213
+ catch (error) {
214
+ writeJson(response, errorStatus(error), { ok: false, created: 0, candidates: selections.length, failed: selections.length, ...errorPayload(error) });
215
+ }
216
+ return;
217
+ }
95
218
  if (requestUrl.pathname === "/api/presets/check" && request.method === "POST") {
96
219
  assertTrustedWorkbenchRequest(request, { origin, csrfToken });
97
220
  const body = await readJsonBody(request);
@@ -159,16 +282,19 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
159
282
  writeJson(response, 405, { error: "Method not allowed" });
160
283
  return;
161
284
  }
285
+ if (request.method === "GET" && isDashboardDataRequest(requestUrl.pathname))
286
+ regenerate();
162
287
  serveStaticFile(response, outputDir, requestUrl.pathname, request.method === "HEAD");
163
288
  }
164
289
  catch (error) {
165
- const status = error.status || (/CSRF|Origin|Host/.test(error.message) ? 403 : 400);
290
+ const message = errorMessage(error);
291
+ const status = errorStatus(error, /CSRF|Origin|Host/.test(message) ? 403 : 400);
166
292
  writeJson(response, status, errorPayload(error));
167
293
  }
168
294
  });
169
295
  await new Promise((resolve, reject) => {
170
296
  server.once("error", reject);
171
- server.listen(Number(port), host, resolve);
297
+ server.listen(Number(port), host, () => resolve());
172
298
  });
173
299
  const address = server.address();
174
300
  const actualPort = typeof address === "object" && address ? address.port : port;
@@ -194,9 +320,83 @@ function normalizePresetScope(value) {
194
320
  throw new Error(`Invalid preset scope: ${scope}`);
195
321
  return scope;
196
322
  }
323
+ function isDashboardDataRequest(urlPath) {
324
+ return urlPath === "/assets/dashboard-data.js" || urlPath.startsWith("/data/");
325
+ }
326
+ function commitWorkbenchBatch(target, allowedPaths, { operation, message }) {
327
+ const paths = uniqueValues(allowedPaths || []);
328
+ const context = beginGovernanceSync(target, {
329
+ operation,
330
+ allowDirtyWorktree: true,
331
+ allowedRelativePaths: paths,
332
+ allowDirtyWriteScope: true,
333
+ });
334
+ try {
335
+ return commitGovernanceSync(context, paths, { message });
336
+ }
337
+ finally {
338
+ releaseGovernanceSync(context);
339
+ }
340
+ }
341
+ function uniqueValues(values) {
342
+ const seen = new Set();
343
+ const result = [];
344
+ for (const value of values) {
345
+ if (seen.has(value))
346
+ continue;
347
+ seen.add(value);
348
+ result.push(value);
349
+ }
350
+ return result;
351
+ }
352
+ function normalizeBulkLessonSelections(rawSelections) {
353
+ if (!Array.isArray(rawSelections))
354
+ return [];
355
+ const seen = new Set();
356
+ const result = [];
357
+ for (const selection of rawSelections) {
358
+ const taskId = String(selection?.taskId || "").trim();
359
+ const candidateId = String(selection?.candidateId || "").trim();
360
+ if (!taskId || !candidateId)
361
+ continue;
362
+ const key = `${taskId}\n${candidateId}`;
363
+ if (seen.has(key))
364
+ continue;
365
+ seen.add(key);
366
+ result.push({ taskId, candidateId });
367
+ }
368
+ return result;
369
+ }
370
+ function findWorkbenchTask(repository, taskId) {
371
+ try {
372
+ return repository.get({ id: taskId });
373
+ }
374
+ catch {
375
+ return undefined;
376
+ }
377
+ }
197
378
  function isTaskInReviewQueue(task) {
198
379
  return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
199
380
  }
381
+ function taskHasPendingLessonWork(task) {
382
+ const queues = Array.isArray(task?.taskQueues) ? task.taskQueues : [];
383
+ const candidateRows = Array.isArray(task?.lessonCandidateRows) ? task.lessonCandidateRows : [];
384
+ return queues.includes("lessons") ||
385
+ task?.lessonCandidateStatus === "needs-promotion" ||
386
+ task?.lessonCandidatePromotionState === "queued" ||
387
+ candidateRows.some((candidate) => ["ready-for-review", "needs-promotion"].includes(String(candidate?.status || "")));
388
+ }
389
+ function taskReadyForCloseout(task) {
390
+ if (!task)
391
+ return false;
392
+ if (task.reviewStatus !== "confirmed")
393
+ return false;
394
+ if (task.closeoutStatus === "closed")
395
+ return false;
396
+ if (taskHasPendingLessonWork(task))
397
+ return false;
398
+ return ["no-candidate-accepted", "promoted", "rejected"].includes(String(task.lessonCandidateStatus || ""));
399
+ }
200
400
  function reviewQueueRejectionPayload(task) {
201
401
  return {
202
402
  error: "Review completion is only available for tasks in the review queue.",
@@ -208,6 +408,18 @@ function reviewQueueRejectionPayload(task) {
208
408
  taskId: task?.id || "",
209
409
  };
210
410
  }
411
+ function closeoutRejectionPayload(task, error = "Task closeout is only available after human review is confirmed and Lesson routing is complete.") {
412
+ return {
413
+ error,
414
+ closeoutStatus: task?.closeoutStatus || "unknown",
415
+ lifecycleState: task?.lifecycleState || "unknown",
416
+ reviewStatus: task?.reviewStatus || "unknown",
417
+ taskQueues: Array.isArray(task?.taskQueues) ? task.taskQueues : [],
418
+ lessonCandidateStatus: task?.lessonCandidateStatus || "unknown",
419
+ lessonCandidatePromotionState: task?.lessonCandidatePromotionState || "unknown",
420
+ taskId: task?.id || "",
421
+ };
422
+ }
211
423
  function startPollingWatch(roots, regenerate) {
212
424
  let lastMtime = latestTreeMtime(roots);
213
425
  let timer = null;
@@ -216,13 +428,14 @@ function startPollingWatch(roots, regenerate) {
216
428
  if (nextMtime <= lastMtime)
217
429
  return;
218
430
  lastMtime = nextMtime;
219
- clearTimeout(timer);
431
+ if (timer)
432
+ clearTimeout(timer);
220
433
  timer = setTimeout(() => {
221
434
  try {
222
435
  regenerate();
223
436
  }
224
437
  catch (error) {
225
- console.error(`dashboard regeneration failed: ${error.message}`);
438
+ console.error(`dashboard regeneration failed: ${errorMessage(error)}`);
226
439
  }
227
440
  }, 250);
228
441
  }, 1000);
@@ -306,15 +519,26 @@ function writeJson(response, status, payload) {
306
519
  response.end(`${JSON.stringify(payload)}\n`);
307
520
  }
308
521
  function errorPayload(error) {
309
- const payload = { error: error.message };
310
- if (error.code)
311
- payload.code = error.code;
312
- if (Array.isArray(error.recovery) && error.recovery.length > 0)
313
- payload.recovery = error.recovery;
314
- if (error.details)
315
- payload.details = error.details;
522
+ const source = isRecord(error) ? error : {};
523
+ const payload = { error: errorMessage(error) };
524
+ if (source.code)
525
+ payload.code = source.code;
526
+ if (Array.isArray(source.recovery) && source.recovery.length > 0)
527
+ payload.recovery = source.recovery;
528
+ if (source.details)
529
+ payload.details = source.details;
316
530
  return payload;
317
531
  }
532
+ function errorStatus(error, fallback = 400) {
533
+ const source = isRecord(error) ? error : {};
534
+ return typeof source.status === "number" ? source.status : fallback;
535
+ }
536
+ function errorMessage(error) {
537
+ return error instanceof Error ? error.message : String(error || "unknown error");
538
+ }
539
+ function isRecord(value) {
540
+ return typeof value === "object" && value !== null;
541
+ }
318
542
  function mimeType(filePath) {
319
543
  if (filePath.endsWith(".html"))
320
544
  return "text/html; charset=utf-8";
@@ -28,6 +28,8 @@ export function writeDashboardDirectory(outDir, bundle, options = {}) {
28
28
  writeJsonFile(path.join(target, "data/tables.json"), bundle.tables);
29
29
  writeJsonFile(path.join(target, "data/documents.json"), bundle.documents);
30
30
  writeJsonFile(path.join(target, "data/graph.json"), bundle.graph);
31
+ writeJsonFile(path.join(target, "data/modules.json"), bundle.modules || []);
32
+ writeJsonFile(path.join(target, "data/moduleSummary.json"), bundle.moduleSummary || {});
31
33
  writeJsonFile(path.join(target, "data/adoption.json"), bundle.adoption);
32
34
  writeJsonFile(path.join(target, "data/presetCatalog.json"), bundle.presetCatalog);
33
35
  fs.writeFileSync(path.join(target, "assets/dashboard-data.js"), `window.__HARNESS_DASHBOARD__ = ${JSON.stringify(bundle, null, 2)};\n`);
@@ -150,7 +152,8 @@ function assertSafeDashboardTarget(target, options) {
150
152
  const entries = fs.readdirSync(target);
151
153
  const hasMarker = fs.existsSync(path.join(target, dashboardMarker));
152
154
  const canRecoverGeneratedDashboard = options.recoverGeneratedDashboard === true && looksLikeGeneratedDashboardDirectory(target);
153
- if (entries.length > 0 && !hasMarker && !canRecoverGeneratedDashboard) {
155
+ const canReplaceExistingOutput = options.replaceExistingDashboardOutput === true;
156
+ if (entries.length > 0 && !hasMarker && !canRecoverGeneratedDashboard && !canReplaceExistingOutput) {
154
157
  throw new Error(`Refusing to overwrite non-dashboard directory: ${target}`);
155
158
  }
156
159
  }
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { inspectGit } from "./governance-sync.mjs";
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  // Governance index rendering stays behavior-first until task/governance surface types are modeled.
3
2
  import fs from "node:fs";
4
3
  import path from "node:path";
@@ -21,9 +20,9 @@ export function rebuildGovernanceIndexes(targetInput, { dryRun = false, archive
21
20
  const changes = surfaces.map((surface) => ({
22
21
  surface: surface.surface,
23
22
  destination: surface.relative,
24
- action: surface.archiveOnly
25
- ? apply ? "archive-legacy-governance-table" : "would-archive-legacy-governance-table"
26
- : apply ? "rebuild-governance-index" : "would-rebuild-governance-index",
23
+ action: isArchiveOnlySurface(surface)
24
+ ? effectiveApply ? "archive-legacy-governance-table" : "would-archive-legacy-governance-table"
25
+ : effectiveApply ? "rebuild-governance-index" : "would-rebuild-governance-index",
27
26
  generatedRows: surface.rows.length,
28
27
  archive: archive ? `${archiveDir}/${surface.relative}` : "",
29
28
  }));
@@ -38,7 +37,7 @@ export function rebuildGovernanceIndexes(targetInput, { dryRun = false, archive
38
37
  fs.copyFileSync(surface.absolute, archivePath);
39
38
  allowed.push(toPosix(path.relative(target.projectRoot, archivePath)));
40
39
  }
41
- if (surface.archiveOnly) {
40
+ if (isArchiveOnlySurface(surface)) {
42
41
  if (archive && fs.existsSync(surface.absolute)) {
43
42
  fs.rmSync(surface.absolute);
44
43
  allowed.push(surface.relative);
@@ -56,6 +55,9 @@ export function rebuildGovernanceIndexes(targetInput, { dryRun = false, archive
56
55
  releaseGovernanceSync(context);
57
56
  }
58
57
  }
58
+ function isArchiveOnlySurface(surface) {
59
+ return "archiveOnly" in surface && surface.archiveOnly === true;
60
+ }
59
61
  function governanceSurfaces(target, tasks) {
60
62
  return [
61
63
  {