coding-agent-harness 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.md +211 -86
  4. package/README.zh-CN.md +54 -34
  5. package/SKILL.md +25 -18
  6. package/docs-release/README.md +9 -5
  7. package/docs-release/architecture/overview.md +17 -5
  8. package/docs-release/architecture/overview.zh-CN.md +9 -5
  9. package/docs-release/assets/dashboard-overview.png +0 -0
  10. package/docs-release/guides/agent-installation.en-US.md +31 -8
  11. package/docs-release/guides/agent-installation.md +34 -9
  12. package/docs-release/guides/contributing.md +100 -0
  13. package/docs-release/guides/contributing.zh-CN.md +99 -0
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  15. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  20. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  21. package/docs-release/guides/migration-playbook.md +14 -15
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  23. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  24. package/docs-release/guides/preset-development.md +214 -0
  25. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  26. package/docs-release/guides/repository-operating-models.md +5 -4
  27. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  28. package/docs-release/guides/task-state-machine.md +214 -0
  29. package/docs-release/intl/en-US.md +1 -1
  30. package/docs-release/intl/zh-CN.md +1 -1
  31. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  32. package/package.json +8 -3
  33. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  34. package/presets/legacy-migration/preset.yaml +134 -0
  35. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  36. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  37. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  38. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  39. package/presets/legacy-migration/templates/review.seed.md +12 -0
  40. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  41. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  42. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  43. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  44. package/presets/lesson-sedimentation/preset.yaml +23 -0
  45. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  46. package/presets/module/preset.yaml +25 -0
  47. package/presets/module/templates/execution_strategy.append.md +8 -0
  48. package/presets/module/templates/task_plan.append.md +17 -0
  49. package/presets/standard-task/preset.yaml +31 -0
  50. package/presets/standard-task/templates/task_plan.append.md +7 -0
  51. package/references/adversarial-review-standard.md +2 -2
  52. package/references/agents-md-pattern.md +2 -2
  53. package/references/delivery-operating-model-standard.md +3 -3
  54. package/references/docs-directory-standard.md +6 -7
  55. package/references/harness-ledger.md +53 -96
  56. package/references/lessons-governance.md +88 -93
  57. package/references/module-parallel-standard.md +14 -14
  58. package/references/planning-loop.md +12 -6
  59. package/references/pull-request-standard.md +118 -0
  60. package/references/repo-governance-standard.md +11 -2
  61. package/references/review-routing-standard.md +7 -1
  62. package/references/ssot-governance.md +67 -59
  63. package/references/taskr-gap-analysis.md +600 -0
  64. package/references/walkthrough-closeout.md +7 -7
  65. package/scripts/check-harness.mjs +40 -301
  66. package/scripts/commands/dashboard-command.mjs +67 -0
  67. package/scripts/commands/migration-command.mjs +96 -0
  68. package/scripts/commands/preset-command.mjs +73 -0
  69. package/scripts/commands/task-command.mjs +327 -0
  70. package/scripts/harness.mjs +55 -260
  71. package/scripts/lib/capability-registry.mjs +66 -8
  72. package/scripts/lib/check-module-parallel.mjs +237 -0
  73. package/scripts/lib/check-profiles.mjs +61 -153
  74. package/scripts/lib/check-task-contracts.mjs +47 -0
  75. package/scripts/lib/core-shared.mjs +10 -0
  76. package/scripts/lib/dashboard-data.mjs +29 -6
  77. package/scripts/lib/dashboard-workbench.mjs +52 -12
  78. package/scripts/lib/dashboard-writer.mjs +14 -2
  79. package/scripts/lib/git-status-summary.mjs +46 -0
  80. package/scripts/lib/governance-index-generator.mjs +174 -0
  81. package/scripts/lib/governance-sync.mjs +514 -0
  82. package/scripts/lib/governance-table-boundary.mjs +175 -0
  83. package/scripts/lib/harness-core.mjs +5 -0
  84. package/scripts/lib/lesson-maintenance.mjs +36 -29
  85. package/scripts/lib/migration-support.mjs +1 -1
  86. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  87. package/scripts/lib/preset-engine.mjs +497 -0
  88. package/scripts/lib/preset-registry.mjs +627 -0
  89. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  90. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  91. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  92. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  93. package/scripts/lib/task-completion-consistency.mjs +16 -0
  94. package/scripts/lib/task-index.mjs +93 -0
  95. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  96. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  97. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  98. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  99. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  100. package/scripts/lib/task-lifecycle.mjs +297 -403
  101. package/scripts/lib/task-review-model.mjs +469 -0
  102. package/scripts/lib/task-scanner.mjs +130 -236
  103. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  104. package/scripts/postinstall.mjs +14 -0
  105. package/skills/preset-creator/SKILL.md +179 -0
  106. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  107. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  108. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  109. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  110. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  111. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  112. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  113. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  114. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  115. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  116. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  117. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  118. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  119. package/templates/AGENTS.md.template +19 -15
  120. package/templates/dashboard/assets/app-src/00-state.js +1 -0
  121. package/templates/dashboard/assets/app-src/10-router.js +2 -1
  122. package/templates/dashboard/assets/app-src/20-overview.js +11 -5
  123. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  124. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  125. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  126. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  127. package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
  128. package/templates/dashboard/assets/app.css +698 -156
  129. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  130. package/templates/dashboard/assets/app.js +662 -91
  131. package/templates/dashboard/assets/app.manifest.json +1 -0
  132. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  133. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  134. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  135. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  136. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  137. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  138. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  139. package/templates/dashboard/assets/i18n.js +123 -21
  140. package/templates/ledger/Harness-Ledger.md +13 -25
  141. package/templates/lessons/lesson-arch-process-change.md +1 -1
  142. package/templates/lessons/lesson-new-doc.md +1 -1
  143. package/templates/lessons/lesson-ref-change.md +1 -1
  144. package/templates/planning/execution_strategy.md +31 -0
  145. package/templates/planning/lesson_candidates.md +18 -6
  146. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  147. package/templates/planning/optional/references/INDEX.md +3 -3
  148. package/templates/planning/review.md +59 -0
  149. package/templates/planning/task_plan.md +36 -13
  150. package/templates/reference/execution-workflow-standard.md +4 -3
  151. package/templates/reference/pull-request-standard.md +80 -0
  152. package/templates/reference/repo-governance-standard.md +7 -6
  153. package/templates/reference/review-routing-standard.md +6 -0
  154. package/templates/reference/walkthrough-standard.md +2 -1
  155. package/templates/verifier/verifier-output.md +1 -1
  156. package/templates-zh-CN/AGENTS.md.template +20 -16
  157. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  158. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  159. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  160. package/templates-zh-CN/planning/review.md +59 -1
  161. package/templates-zh-CN/planning/task_plan.md +30 -10
  162. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  163. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  164. package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
  165. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  166. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  167. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  168. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  169. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  170. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  171. package/docs-release/assets/dashboard-overview-en.png +0 -0
  172. package/scripts/smoke-dashboard.mjs +0 -92
  173. package/scripts/test-harness.mjs +0 -1395
  174. package/templates/ssot/Feature-SSoT.md +0 -43
  175. package/templates/ssot/Lessons-SSoT.md +0 -44
  176. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  177. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -56,7 +56,6 @@ function collectDashboardDocumentPaths(target) {
56
56
  "09-PLANNING/Module-Registry.md",
57
57
  "05-TEST-QA/Regression-SSoT.md",
58
58
  "05-TEST-QA/Cadence-Ledger.md",
59
- "01-GOVERNANCE/Lessons-SSoT.md",
60
59
  "10-WALKTHROUGH/Closeout-SSoT.md",
61
60
  ]) {
62
61
  addDocsPath(relativePath);
@@ -104,7 +103,7 @@ function documentKind(source) {
104
103
  if (lower.includes("module-registry.md")) return "module-registry";
105
104
  if (lower.includes("regression-ssot.md")) return "regression-ssot";
106
105
  if (lower.includes("cadence-ledger.md")) return "cadence-ledger";
107
- if (lower.includes("lessons-ssot.md")) return "lessons-ssot";
106
+ if (/\/01-governance\/lessons\/[^/]+\.md$/i.test(lower)) return "lesson-detail";
108
107
  if (lower.endsWith("/progress.md")) return "task-progress";
109
108
  if (lower.endsWith("/brief.md")) return "task-brief";
110
109
  if (lower.endsWith("/review.md")) return "task-review";
@@ -200,6 +199,7 @@ export function collectGraph(status, tables = { tables: [] }) {
200
199
  }
201
200
 
202
201
  export function categorizeWarning(message) {
202
+ if (/governance-table-entropy/i.test(message)) return "Governance Table Boundary";
203
203
  if (/missing execution_strategy\.md|missing visual_(?:map|roadmap)\.md|Visual (?:Map|Roadmap)/i.test(message)) return "Plan Contract Missing";
204
204
  if (/legacy-compat|adoption-needed|legacy check/i.test(message)) return "Adoption Advice";
205
205
  if (/Evidence|evidence/i.test(message)) return "Missing Evidence";
@@ -213,6 +213,7 @@ function warningType(message) {
213
213
  if (/missing visual_map\.md|Visual Map/i.test(message)) return "missing-visual-map";
214
214
  if (/missing visual_roadmap\.md|Visual Roadmap/i.test(message)) return "missing-visual-roadmap";
215
215
  if (/Reviewer Identity|Confidence Challenge|Final Confidence Basis|Evidence Checked/i.test(message)) return "review-schema-gap";
216
+ if (/governance-table-entropy/i.test(message)) return "governance-table-entropy";
216
217
  if (/Evidence|evidence/i.test(message)) return "missing-evidence";
217
218
  if (/missing required file/i.test(message)) return "legacy-reference-gap";
218
219
  if (/legacy-compat|legacy check|adoption-needed/i.test(message)) return "capability-adoption";
@@ -231,6 +232,7 @@ function warningScope(message) {
231
232
 
232
233
  function warningPhase(type, scope) {
233
234
  if (type === "capability-adoption") return "baseline";
235
+ if (type === "governance-table-entropy") return "global-table-boundary";
234
236
  if (type === "missing-brief" || type === "missing-execution-strategy" || type === "missing-visual-map" || type === "missing-visual-roadmap") return "active-task-contracts";
235
237
  if (scope === "module") return "module-classification";
236
238
  if (type === "review-schema-gap" || type === "missing-evidence") return "review-evidence";
@@ -240,6 +242,7 @@ function warningPhase(type, scope) {
240
242
 
241
243
  function warningFixability(type, scope) {
242
244
  if (["missing-brief", "missing-execution-strategy", "missing-visual-map", "missing-visual-roadmap"].includes(type)) return "guided";
245
+ if (type === "governance-table-entropy") return "manual";
243
246
  if (type === "legacy-reference-gap" || scope === "reference") return "template";
244
247
  if (type === "capability-adoption") return "decision";
245
248
  if (type === "review-schema-gap" || type === "missing-evidence") return "human-evidence";
@@ -248,6 +251,7 @@ function warningFixability(type, scope) {
248
251
 
249
252
  function warningPriority(type, scope, message) {
250
253
  if (/fail|invalid|blocked/i.test(message) || type === "schema-drift") return "P1";
254
+ if (type === "governance-table-entropy") return /legacy-report-only/i.test(message) ? "P3" : "P2";
251
255
  if (["missing-brief", "missing-execution-strategy", "missing-visual-map", "missing-visual-roadmap"].includes(type) && scope === "task") return "P2";
252
256
  if (type === "review-schema-gap" || type === "missing-evidence") return "P2";
253
257
  if (type === "capability-adoption") return "P3";
@@ -284,19 +288,24 @@ function summarizeWarnings(warnings) {
284
288
  }
285
289
 
286
290
  export function collectAdoption(status) {
287
- const warnings = status.checkState.details.warnings.flatMap((message) => splitWarningMessage(message)).map((message, index) => {
291
+ const dashboardMessages = [
292
+ ...(status.checkState.details.warnings || []),
293
+ ...(status.checkState.details.failures || []).filter((message) => /governance-table-entropy/i.test(message)),
294
+ ];
295
+ const warnings = dashboardMessages.flatMap((message) => splitWarningMessage(message)).map((message, index) => {
288
296
  const type = warningType(message);
289
297
  const scope = warningScope(message);
290
298
  const affectedPaths = warningAffectedPaths(message);
299
+ const stableSuffix = type === "governance-table-entropy" ? `-${stableWarningIdPart(governanceWarningRowKey(message))}` : "";
291
300
  return {
292
- id: `AD-${String(index + 1).padStart(3, "0")}`,
301
+ id: `AD-${String(index + 1).padStart(3, "0")}${stableSuffix}`,
293
302
  category: categorizeWarning(message),
294
303
  type,
295
304
  scope,
296
305
  priority: warningPriority(type, scope, message),
297
306
  phase: warningPhase(type, scope),
298
307
  fixability: warningFixability(type, scope),
299
- status: "open",
308
+ status: /legacy-report-only/i.test(message) ? "legacy-report-only" : "open",
300
309
  confidence: warningConfidence(message),
301
310
  severity: status.mode === "legacy-compat" ? "advice" : "warning",
302
311
  title: warningTitle(message),
@@ -330,6 +339,18 @@ export function collectAdoption(status) {
330
339
  };
331
340
  }
332
341
 
342
+ function governanceWarningRowKey(message) {
343
+ const match = String(message || "").match(/\brow\s+([^:]+)/i);
344
+ return match ? match[1].trim() : "global-table";
345
+ }
346
+
347
+ function stableWarningIdPart(value) {
348
+ return String(value || "global-table")
349
+ .replace(/[^A-Za-z0-9_-]+/g, "-")
350
+ .replace(/^-+|-+$/g, "")
351
+ .slice(0, 80) || "global-table";
352
+ }
353
+
333
354
  export function splitWarningMessage(message) {
334
355
  return String(message || "")
335
356
  .split(/\n-\s+/)
@@ -338,6 +359,7 @@ export function splitWarningMessage(message) {
338
359
  }
339
360
 
340
361
  function warningTitle(message) {
362
+ if (/governance-table-entropy/i.test(message)) return "Global table boundary";
341
363
  if (/missing execution_strategy\.md/i.test(message)) return "Missing execution strategy";
342
364
  if (/missing visual_map\.md|Visual Map/i.test(message)) return "Missing visual map";
343
365
  if (/missing visual_roadmap\.md|Visual Roadmap/i.test(message)) return "Missing legacy visual roadmap";
@@ -354,6 +376,7 @@ function warningAffected(message) {
354
376
  }
355
377
 
356
378
  function warningAction(message) {
379
+ if (/governance-table-entropy/i.test(message)) return "Move local detail to module/task docs; keep the global row to summary, state, route, and audit result.";
357
380
  if (/execution_strategy\.md/i.test(message)) return "Add standalone execution strategy file.";
358
381
  if (/visual_map\.md|Visual Map/i.test(message)) return "Add standalone visual map file.";
359
382
  if (/visual_roadmap\.md|Visual Roadmap/i.test(message)) return "Rewrite legacy visual_roadmap.md into canonical visual_map.md.";
@@ -377,7 +400,7 @@ export function writeDashboardFolder(outDir, targetInput, options = {}) {
377
400
  const registry = readCapabilityRegistry(target);
378
401
  const locale = options.localeOverride || registry.locale;
379
402
  const bundle = buildDashboardBundle(targetInput, options);
380
- return writeDashboardDirectory(outDir, bundle, { repoRoot, projectRoot: target.projectRoot, docsRoot: target.docsRoot, locale, workbenchRuntime: options.workbenchRuntime === true });
403
+ return writeDashboardDirectory(outDir, bundle, { repoRoot, projectRoot: target.projectRoot, docsRoot: target.docsRoot, locale, workbenchRuntime: options.workbenchRuntime === true, recoverGeneratedDashboard: options.recoverGeneratedDashboard === true });
381
404
  }
382
405
 
383
406
  export function writeDashboardSingleFile(outFile, targetInput, options = {}) {
@@ -5,13 +5,14 @@ import http from "node:http";
5
5
  import path from "node:path";
6
6
  import { URL } from "node:url";
7
7
  import { confirmTaskReview } from "./task-lifecycle.mjs";
8
+ import { createLessonSedimentationTask } from "./task-lesson-sedimentation.mjs";
8
9
  import { normalizeTarget } from "./core-shared.mjs";
9
10
  import { collectTasks } from "./task-scanner.mjs";
10
11
  import { writeDashboardFolder } from "./dashboard-data.mjs";
11
12
 
12
13
  const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
13
14
 
14
- export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench" } = {}) {
15
+ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench", recoverGeneratedDashboard = false } = {}) {
15
16
  if (host !== "127.0.0.1") throw new Error("dashboard workbench only supports --host 127.0.0.1");
16
17
  const target = normalizeTarget(targetInput);
17
18
  const outputDir = path.resolve(outDir);
@@ -19,7 +20,7 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
19
20
  const options = localeOverride ? { localeOverride } : {};
20
21
  let snapshotVersion = Date.now();
21
22
  const regenerate = () => {
22
- writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true });
23
+ writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true, recoverGeneratedDashboard });
23
24
  snapshotVersion = Date.now();
24
25
  };
25
26
  regenerate();
@@ -35,7 +36,7 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
35
36
  writeJson(response, 200, {
36
37
  mode: "workbench",
37
38
  csrfToken,
38
- writableActions: ["review-complete"],
39
+ writableActions: ["review-complete", "lesson-sedimentation-task"],
39
40
  target: target.projectRoot,
40
41
  autoRefresh: autoRefresh === true,
41
42
  snapshotVersion,
@@ -52,8 +53,8 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
52
53
  writeJson(response, 404, { error: "Task not found" });
53
54
  return;
54
55
  }
55
- if (!isTaskInReviewStage(task)) {
56
- writeJson(response, 409, { error: "Review completion is only available while the task is in review." });
56
+ if (!isTaskInReviewQueue(task)) {
57
+ writeJson(response, 409, reviewQueueRejectionPayload(task));
57
58
  return;
58
59
  }
59
60
  if (task.reviewStatus === "confirmed") {
@@ -71,14 +72,36 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
71
72
  return;
72
73
  }
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
+
74
97
  if (request.method !== "GET" && request.method !== "HEAD") {
75
98
  writeJson(response, 405, { error: "Method not allowed" });
76
99
  return;
77
100
  }
78
101
  serveStaticFile(response, outputDir, requestUrl.pathname, request.method === "HEAD");
79
102
  } catch (error) {
80
- const status = /CSRF|Origin|Host/.test(error.message) ? 403 : 400;
81
- writeJson(response, status, { error: error.message });
103
+ const status = error.status || (/CSRF|Origin|Host/.test(error.message) ? 403 : 400);
104
+ writeJson(response, status, errorPayload(error));
82
105
  }
83
106
  });
84
107
 
@@ -103,11 +126,20 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
103
126
  await new Promise(() => {});
104
127
  }
105
128
 
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);
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
+ };
111
143
  }
112
144
 
113
145
  function startPollingWatch(root, regenerate) {
@@ -202,6 +234,14 @@ function writeJson(response, status, payload) {
202
234
  response.end(`${JSON.stringify(payload)}\n`);
203
235
  }
204
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
+
205
245
  function mimeType(filePath) {
206
246
  if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
207
247
  if (filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
@@ -18,10 +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
+ fs.mkdirSync(target, { recursive: true });
22
+ fs.writeFileSync(path.join(target, dashboardMarker), "generated dashboard directory\n");
21
23
  copyDashboardAssets(target, options);
22
24
  fs.writeFileSync(path.join(target, "assets/app.js"), readDashboardApp(dashboardTemplateRootForLocale(options.locale)));
23
25
  fs.writeFileSync(path.join(target, "index.html"), renderDashboardIndex(options.locale, options));
24
- fs.writeFileSync(path.join(target, dashboardMarker), "generated dashboard directory\n");
25
26
  writeJsonFile(path.join(target, "data/status.json"), bundle.status);
26
27
  writeJsonFile(path.join(target, "data/tables.json"), bundle.tables);
27
28
  writeJsonFile(path.join(target, "data/documents.json"), bundle.documents);
@@ -156,12 +157,23 @@ function assertSafeDashboardTarget(target, options) {
156
157
  if (fs.existsSync(target)) {
157
158
  const entries = fs.readdirSync(target);
158
159
  const hasMarker = fs.existsSync(path.join(target, dashboardMarker));
159
- if (entries.length > 0 && !hasMarker) {
160
+ const canRecoverGeneratedDashboard = options.recoverGeneratedDashboard === true && looksLikeGeneratedDashboardDirectory(target);
161
+ if (entries.length > 0 && !hasMarker && !canRecoverGeneratedDashboard) {
160
162
  throw new Error(`Refusing to overwrite non-dashboard directory: ${target}`);
161
163
  }
162
164
  }
163
165
  }
164
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
+
165
177
  function dashboardTemplateRootForLocale(locale = "en-US") {
166
178
  return dashboardTemplateRoot;
167
179
  }
@@ -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
+ }