coding-agent-harness 1.0.2 → 1.0.5

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 (219) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/LICENSE +661 -21
  4. package/LICENSE-EXCEPTION.md +37 -0
  5. package/README.md +244 -87
  6. package/README.zh-CN.md +77 -35
  7. package/SKILL.md +32 -24
  8. package/docs-release/README.md +9 -5
  9. package/docs-release/architecture/overview.md +17 -5
  10. package/docs-release/architecture/overview.zh-CN.md +9 -5
  11. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  12. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  13. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  14. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  15. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  16. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  17. package/docs-release/architecture/system-explainer/README.md +67 -0
  18. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  19. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  20. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  21. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  22. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  23. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  24. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  25. package/docs-release/assets/dashboard-overview.png +0 -0
  26. package/docs-release/guides/agent-installation.en-US.md +39 -15
  27. package/docs-release/guides/agent-installation.md +43 -16
  28. package/docs-release/guides/contributing.md +100 -0
  29. package/docs-release/guides/contributing.zh-CN.md +99 -0
  30. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  31. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  32. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  33. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  34. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  35. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  36. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  37. package/docs-release/guides/migration-playbook.md +14 -15
  38. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  39. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  40. package/docs-release/guides/preset-development.md +238 -0
  41. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  42. package/docs-release/guides/repository-operating-models.md +5 -4
  43. package/docs-release/guides/task-state-machine.en-US.md +224 -0
  44. package/docs-release/guides/task-state-machine.md +231 -0
  45. package/docs-release/intl/en-US.md +1 -1
  46. package/docs-release/intl/zh-CN.md +1 -1
  47. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  48. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  49. package/package.json +10 -4
  50. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  51. package/presets/legacy-migration/preset.yaml +134 -0
  52. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  53. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  54. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  55. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  56. package/presets/legacy-migration/templates/review.seed.md +12 -0
  57. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  58. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  59. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  60. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  61. package/presets/lesson-sedimentation/preset.yaml +23 -0
  62. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  63. package/presets/module/preset.yaml +25 -0
  64. package/presets/module/templates/execution_strategy.append.md +8 -0
  65. package/presets/module/templates/task_plan.append.md +17 -0
  66. package/presets/standard-task/preset.yaml +31 -0
  67. package/presets/standard-task/templates/task_plan.append.md +7 -0
  68. package/references/adversarial-review-standard.md +2 -2
  69. package/references/agents-md-pattern.md +2 -2
  70. package/references/delivery-operating-model-standard.md +3 -3
  71. package/references/docs-directory-standard.md +6 -7
  72. package/references/harness-ledger.md +53 -96
  73. package/references/lessons-governance.md +88 -93
  74. package/references/module-parallel-standard.md +14 -14
  75. package/references/planning-loop.md +12 -6
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +11 -2
  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/walkthrough-closeout.md +7 -7
  82. package/scripts/check-harness.mjs +40 -301
  83. package/scripts/commands/dashboard-command.mjs +67 -0
  84. package/scripts/commands/migration-command.mjs +126 -0
  85. package/scripts/commands/preset-command.mjs +73 -0
  86. package/scripts/commands/task-command.mjs +328 -0
  87. package/scripts/harness.mjs +59 -260
  88. package/scripts/lib/capability-registry.mjs +82 -28
  89. package/scripts/lib/check-module-parallel.mjs +230 -0
  90. package/scripts/lib/check-profiles.mjs +90 -228
  91. package/scripts/lib/check-task-contracts.mjs +55 -0
  92. package/scripts/lib/core-shared.mjs +65 -2
  93. package/scripts/lib/dashboard-data.mjs +155 -24
  94. package/scripts/lib/dashboard-workbench.mjs +131 -12
  95. package/scripts/lib/dashboard-writer.mjs +20 -4
  96. package/scripts/lib/git-status-summary.mjs +46 -0
  97. package/scripts/lib/governance-index-generator.mjs +174 -0
  98. package/scripts/lib/governance-sync.mjs +611 -0
  99. package/scripts/lib/governance-table-boundary.mjs +175 -0
  100. package/scripts/lib/harness-core.mjs +6 -0
  101. package/scripts/lib/lesson-maintenance.mjs +36 -29
  102. package/scripts/lib/markdown-utils.mjs +33 -0
  103. package/scripts/lib/migration-planner.mjs +4 -6
  104. package/scripts/lib/migration-support.mjs +1 -1
  105. package/scripts/lib/phase-kind.mjs +50 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +494 -0
  108. package/scripts/lib/preset-registry.mjs +776 -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-builder.mjs +88 -0
  112. package/scripts/lib/status-dashboard-renderer.mjs +105 -0
  113. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  114. package/scripts/lib/task-audit-metadata.mjs +385 -0
  115. package/scripts/lib/task-audit-migration.mjs +350 -0
  116. package/scripts/lib/task-completion-consistency.mjs +26 -0
  117. package/scripts/lib/task-index.mjs +93 -0
  118. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  119. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  120. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  121. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  122. package/scripts/lib/task-lifecycle/review-confirm.mjs +112 -0
  123. package/scripts/lib/task-lifecycle/review-gates.mjs +73 -0
  124. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  125. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  126. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  127. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  128. package/scripts/lib/task-lifecycle.mjs +338 -477
  129. package/scripts/lib/task-metadata.mjs +118 -0
  130. package/scripts/lib/task-review-model.mjs +455 -0
  131. package/scripts/lib/task-scanner.mjs +193 -372
  132. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  133. package/scripts/postinstall.mjs +14 -0
  134. package/skills/preset-creator/SKILL.md +179 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  137. package/skills/preset-creator/references/complex-task-skeleton/brief.md +43 -0
  138. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  139. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  140. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  141. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  142. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  143. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  144. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  145. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  146. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  147. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  148. package/templates/AGENTS.md.template +24 -18
  149. package/templates/dashboard/assets/app-src/00-state.js +13 -0
  150. package/templates/dashboard/assets/app-src/10-router.js +5 -1
  151. package/templates/dashboard/assets/app-src/20-overview.js +18 -8
  152. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  153. package/templates/dashboard/assets/app-src/35-task-detail.js +286 -0
  154. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  155. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  156. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  157. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  158. package/templates/dashboard/assets/app-src/90-bindings.js +302 -29
  159. package/templates/dashboard/assets/app.css +1501 -376
  160. package/templates/dashboard/assets/app.css.manifest.json +10 -0
  161. package/templates/dashboard/assets/app.js +1240 -101
  162. package/templates/dashboard/assets/app.manifest.json +2 -0
  163. package/templates/dashboard/assets/css-src/00-foundation.css +346 -0
  164. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  165. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  166. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  167. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  168. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +489 -0
  169. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  170. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  171. package/templates/dashboard/assets/i18n.js +263 -23
  172. package/templates/ledger/Harness-Ledger.md +13 -25
  173. package/templates/lessons/lesson-arch-process-change.md +1 -1
  174. package/templates/lessons/lesson-new-doc.md +1 -1
  175. package/templates/lessons/lesson-ref-change.md +1 -1
  176. package/templates/planning/INDEX.md +87 -0
  177. package/templates/planning/brief.md +1 -1
  178. package/templates/planning/execution_strategy.md +31 -0
  179. package/templates/planning/lesson_candidates.md +18 -6
  180. package/templates/planning/module_session_prompt.md +1 -0
  181. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  182. package/templates/planning/optional/references/INDEX.md +3 -3
  183. package/templates/planning/review.md +41 -0
  184. package/templates/planning/task_plan.md +5 -21
  185. package/templates/planning/visual_map.md +13 -9
  186. package/templates/planning/visual_map.simple.md +52 -0
  187. package/templates/reference/execution-workflow-standard.md +31 -3
  188. package/templates/reference/pull-request-standard.md +80 -0
  189. package/templates/reference/repo-governance-standard.md +7 -6
  190. package/templates/reference/review-routing-standard.md +6 -0
  191. package/templates/reference/walkthrough-standard.md +2 -1
  192. package/templates/verifier/verifier-output.md +1 -1
  193. package/templates-zh-CN/AGENTS.md.template +25 -19
  194. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  195. package/templates-zh-CN/planning/INDEX.md +87 -0
  196. package/templates-zh-CN/planning/brief.md +1 -1
  197. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  198. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  199. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  200. package/templates-zh-CN/planning/review.md +41 -1
  201. package/templates-zh-CN/planning/task_plan.md +4 -44
  202. package/templates-zh-CN/planning/visual_map.md +14 -7
  203. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  204. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  205. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  206. package/templates-zh-CN/reference/execution-workflow-standard.md +33 -7
  207. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  208. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  209. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  210. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  211. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  212. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  213. package/docs-release/assets/dashboard-overview-en.png +0 -0
  214. package/scripts/smoke-dashboard.mjs +0 -92
  215. package/scripts/test-harness.mjs +0 -1395
  216. package/templates/ssot/Feature-SSoT.md +0 -43
  217. package/templates/ssot/Lessons-SSoT.md +0 -44
  218. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  219. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -0,0 +1,230 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { walkFiles } from "./core-shared.mjs";
4
+
5
+ function stripMarkdownCode(value) {
6
+ return String(value || "").replace(/`/g, "").trim();
7
+ }
8
+
9
+ function modulePromptBlock(content, key) {
10
+ const heading = `## Module: ${key}`;
11
+ const start = content.indexOf(heading);
12
+ if (start < 0) return "";
13
+ const rest = content.slice(start + heading.length);
14
+ const next = rest.search(/\n## Module: /);
15
+ return next >= 0 ? rest.slice(0, next) : rest;
16
+ }
17
+
18
+ function listModuleTaskPlans({ targetRoot, rel, filePath }) {
19
+ const modulesRoot = filePath("docs/09-PLANNING/MODULES");
20
+ if (!fs.existsSync(modulesRoot)) return [];
21
+ return walkFiles(modulesRoot, {
22
+ dirFilter: (_dirName, fullPath) => {
23
+ const relativePath = rel(path.relative(targetRoot, fullPath));
24
+ return !relativePath.includes("/_archive/") && !relativePath.endsWith("/_task-template");
25
+ },
26
+ })
27
+ .map((file) => rel(path.relative(targetRoot, file)))
28
+ .filter((relativePath) => /\/TASKS\/[^/]+\/task_plan\.md$/.test(relativePath));
29
+ }
30
+
31
+ function parseModuleTaskPath(taskPlanPath) {
32
+ const match = taskPlanPath.match(/^docs\/09-PLANNING\/MODULES\/([^/]+)\/TASKS\/([^/]+)\/task_plan\.md$/);
33
+ if (!match) return null;
34
+ return { moduleKey: match[1], taskDir: match[2] };
35
+ }
36
+
37
+ function extractStepId(taskPlanContent, taskDir) {
38
+ const fromPlan = taskPlanContent.match(/^- Step ID:\s*`?([A-Z]{2,5}-\d{2})`?/m);
39
+ if (fromPlan) return fromPlan[1];
40
+ const fromModuleSection = taskPlanContent.match(/^- Step:\s*`?([A-Z]{2,5}-\d{2})`?/m);
41
+ if (fromModuleSection) return fromModuleSection[1];
42
+ const fromDir = taskDir.match(/^([A-Z]{2,5}-\d{2})-/);
43
+ return fromDir ? fromDir[1] : "";
44
+ }
45
+
46
+ function readTaskProgress(taskPlanPath, { exists, read }) {
47
+ const progressPath = taskPlanPath.replace(/task_plan\.md$/, "progress.md");
48
+ return exists(progressPath) ? read(progressPath) : "";
49
+ }
50
+
51
+ function normalizeModuleTaskStatus(status) {
52
+ const value = String(status || "").trim().toLowerCase();
53
+ const aliases = new Map([
54
+ ["未开始", "not-started"],
55
+ ["未启动", "not-started"],
56
+ ["进行中", "in-progress"],
57
+ ["开发中", "in-progress"],
58
+ ["规划审查", "planning-review"],
59
+ ["已完成", "completed"],
60
+ ["完成", "completed"],
61
+ ["已关闭", "closed"],
62
+ ["关闭", "closed"],
63
+ ["已阻塞", "blocked"],
64
+ ["阻塞", "blocked"],
65
+ ]);
66
+ return aliases.get(value) || value;
67
+ }
68
+
69
+ function readTaskProgressStatus(taskPlanPath, context) {
70
+ const progress = readTaskProgress(taskPlanPath, context);
71
+ if (!progress) return "";
72
+ const match = progress.match(/^##\s*(?:Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
73
+ return match ? normalizeModuleTaskStatus(stripMarkdownCode(match[1])) : "";
74
+ }
75
+
76
+ function isActiveModuleTaskStatus(status) {
77
+ if (!status) return false;
78
+ return !new Set([
79
+ "not-started",
80
+ "blocked-not-started",
81
+ "complete",
82
+ "completed",
83
+ "closed",
84
+ "closed-with-residual",
85
+ "closed-local-only",
86
+ "superseded",
87
+ ]).has(status);
88
+ }
89
+
90
+ function hasPendingCoordinatorHandoff(taskPlanContent, progressContent) {
91
+ const combined = `${taskPlanContent}\n${progressContent}`;
92
+ return /Coordinator Handoff/i.test(combined) && /pending-coordinator-pass/i.test(combined);
93
+ }
94
+
95
+ function checkModuleTaskSsotIndex(registryRows, context) {
96
+ const { exists, read, fail, warn, requireGlobalModuleSync } = context;
97
+ const registryByModule = new Map(registryRows.map((cells) => [cells[0], cells]));
98
+ const ledgerContent = exists("docs/Harness-Ledger.md") ? read("docs/Harness-Ledger.md") : "";
99
+ const taskPlans = listModuleTaskPlans(context);
100
+
101
+ for (const taskPlanPath of taskPlans) {
102
+ const parsed = parseModuleTaskPath(taskPlanPath);
103
+ if (!parsed) continue;
104
+ const { moduleKey, taskDir } = parsed;
105
+ const modulePlanPath = `docs/09-PLANNING/MODULES/${moduleKey}/module_plan.md`;
106
+ if (!exists(modulePlanPath)) continue;
107
+
108
+ const taskPlan = read(taskPlanPath);
109
+ const taskProgress = readTaskProgress(taskPlanPath, context);
110
+ const taskProgressStatus = readTaskProgressStatus(taskPlanPath, context);
111
+ const taskIsActive = isActiveModuleTaskStatus(taskProgressStatus);
112
+ const stepId = extractStepId(taskPlan, taskDir);
113
+ if (!stepId) {
114
+ if (taskIsActive) {
115
+ fail(`${taskPlanPath} does not expose a Step ID and task directory does not start with <PREFIX-NN>`);
116
+ }
117
+ continue;
118
+ }
119
+
120
+ const modulePlan = read(modulePlanPath);
121
+ const moduleRelativeTaskPlan = `TASKS/${taskDir}/task_plan.md`;
122
+ if (!modulePlan.includes(stepId) || !modulePlan.includes(moduleRelativeTaskPlan)) {
123
+ fail(`${modulePlanPath} does not index ${stepId} task plan ${moduleRelativeTaskPlan}`);
124
+ }
125
+
126
+ if (!taskIsActive) continue;
127
+
128
+ const registryRow = registryByModule.get(moduleKey);
129
+ const reviewPath = taskPlanPath.replace(/task_plan\.md$/, "review.md");
130
+ const registrySynced = Boolean(registryRow && registryRow[4] === stepId);
131
+ const ledgerSynced = ledgerContent.includes(taskPlanPath) && (!exists(reviewPath) || ledgerContent.includes(reviewPath));
132
+ if (registrySynced && ledgerSynced) continue;
133
+
134
+ if (requireGlobalModuleSync) {
135
+ if (!registryRow) {
136
+ fail(`docs/09-PLANNING/Module-Registry.md does not include active module ${moduleKey} for ${taskPlanPath}`);
137
+ } else if (registryRow[4] !== stepId) {
138
+ fail(`docs/09-PLANNING/Module-Registry.md row ${moduleKey} current step is ${registryRow[4]}, but active task is ${stepId}`);
139
+ }
140
+ if (!ledgerContent.includes(taskPlanPath)) {
141
+ fail(`docs/Harness-Ledger.md does not index active module task plan ${taskPlanPath}`);
142
+ }
143
+ if (exists(reviewPath) && !ledgerContent.includes(reviewPath)) {
144
+ fail(`docs/Harness-Ledger.md does not index active module review ${reviewPath}`);
145
+ }
146
+ continue;
147
+ }
148
+
149
+ if (hasPendingCoordinatorHandoff(taskPlan, taskProgress)) {
150
+ warn(`${taskPlanPath} has pending coordinator handoff; run coordinator pass before final integration or set HARNESS_REQUIRE_GLOBAL_MODULE_SYNC=1 for strict gate`);
151
+ continue;
152
+ }
153
+ fail(`${taskPlanPath} is active but is neither globally synced nor marked with Coordinator Handoff: pending-coordinator-pass`);
154
+ }
155
+ }
156
+
157
+ export function checkModuleParallelStructure(context) {
158
+ const { exists, read, fail, requireFile, markdownTable } = context;
159
+ if (!exists("docs/09-PLANNING/Module-Registry.md")) return;
160
+
161
+ requireFile("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md");
162
+ const hasPromptPack = exists("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md");
163
+ for (const templateFile of [
164
+ "docs/09-PLANNING/MODULES/_task-template/task_plan.md",
165
+ "docs/09-PLANNING/MODULES/_task-template/progress.md",
166
+ "docs/09-PLANNING/MODULES/_task-template/findings.md",
167
+ "docs/09-PLANNING/MODULES/_task-template/review.md",
168
+ ]) {
169
+ requireFile(templateFile);
170
+ }
171
+
172
+ const registryContent = read("docs/09-PLANNING/Module-Registry.md");
173
+ for (const term of ["PREFIX", "Current Step", "Status", "Write Scope"]) {
174
+ if (!registryContent.includes(term)) {
175
+ fail(`docs/09-PLANNING/Module-Registry.md missing registry column or section: ${term}`);
176
+ }
177
+ }
178
+
179
+ const registryRows = markdownTable(registryContent)
180
+ .filter((cells) => cells.length >= 6)
181
+ .filter((cells) => /^(_shared|[a-z][a-z0-9-]*)$/.test(cells[0] || "") && /^[A-Z]{2,5}$/.test(cells[2] || ""));
182
+
183
+ if (registryRows.length === 0) {
184
+ fail("docs/09-PLANNING/Module-Registry.md has no active module rows");
185
+ }
186
+
187
+ const promptPack = hasPromptPack ? read("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md") : "";
188
+ if (hasPromptPack && !/Subagent Worker Invariant|worker[\s\S]{0,120}worktree[\s\S]{0,120}commit SHA/i.test(promptPack)) {
189
+ fail("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md missing subagent worker worktree/commit handoff rule");
190
+ }
191
+ for (const cells of registryRows) {
192
+ const [key, , prefix, branch, currentStep, status] = cells;
193
+ requireFile(`docs/09-PLANNING/MODULES/${key}/module_plan.md`);
194
+ if (!/^(planned|in-progress|paused|completed)$/.test(status)) {
195
+ fail(`docs/09-PLANNING/Module-Registry.md row ${key} has invalid status: ${status}`);
196
+ }
197
+ if (currentStep !== `${prefix}-00` && !currentStep.startsWith(`${prefix}-`)) {
198
+ fail(`docs/09-PLANNING/Module-Registry.md row ${key} current step does not match prefix ${prefix}: ${currentStep}`);
199
+ }
200
+ const branchName = stripMarkdownCode(branch);
201
+ if (!branchName.startsWith("codex/")) {
202
+ fail(`docs/09-PLANNING/Module-Registry.md row ${key} branch must use codex/ prefix: ${branch}`);
203
+ }
204
+
205
+ const block = modulePromptBlock(promptPack, key);
206
+ if (!block) {
207
+ if (!exists(`docs/09-PLANNING/MODULES/${key}/session_prompt.md`)) {
208
+ fail(`missing module session prompt for ${key}`);
209
+ }
210
+ continue;
211
+ }
212
+ for (const term of [
213
+ "Current Step",
214
+ branchName,
215
+ "Preflight:",
216
+ "Before code edits:",
217
+ "Write scope:",
218
+ "Forbidden without coordination:",
219
+ "Shared Coordination:",
220
+ "Verification:",
221
+ "Closeout:",
222
+ "Stop conditions:",
223
+ ]) {
224
+ if (!block.includes(term)) {
225
+ fail(`module session prompt for ${key} missing required term: ${term}`);
226
+ }
227
+ }
228
+ }
229
+ checkModuleTaskSsotIndex(registryRows, context);
230
+ }
@@ -6,7 +6,6 @@ import {
6
6
  legacyChecker,
7
7
  visualMapFile,
8
8
  legacyVisualRoadmapFile,
9
- lessonCandidatesFile,
10
9
  allowedReviewDispositions,
11
10
  allowedPhaseStates,
12
11
  allowedEvidenceStatus,
@@ -23,19 +22,20 @@ import {
23
22
  firstColumn,
24
23
  contentHasAny,
25
24
  } from "./markdown-utils.mjs";
26
- import {
27
- capabilityDefinitions,
28
- validateCapabilities,
29
- } from "./capability-registry.mjs";
30
- import {
31
- collectTasks,
32
- listTaskPlanPaths,
33
- parseTaskBudget,
34
- parseTaskContractInfo,
35
- readVisualMapContractFile,
36
- parsePhases,
37
- taskCutoverCounters,
38
- } from "./task-scanner.mjs";
25
+ import { validateCapabilities } from "./capability-registry.mjs";
26
+ import { readPresetPackage } from "./preset-registry.mjs";
27
+ import { validateTaskPresetAuditSnapshot } from "./preset-audit-contracts.mjs";
28
+ import { validatePresetResourcesForTask } from "./preset-resource-contracts.mjs";
29
+ import { collectTasks, listTaskPlanPaths, parseTaskBudget, readVisualMapContractFile, parsePhases } from "./task-scanner.mjs";
30
+ import { normalizeReviewBoolean, reviewFindingColumns } from "./task-review-model.mjs";
31
+ import { allowedPhaseActors, allowedPhaseKinds } from "./phase-kind.mjs";
32
+ import { validateTaskCompletionConsistency } from "./task-completion-consistency.mjs";
33
+ import { validatePlanContracts } from "./check-task-contracts.mjs";
34
+ import { validateGovernanceTableBoundaries } from "./governance-table-boundary.mjs";
35
+ import { validateSubagentAuthorization } from "./subagent-authorization-audit.mjs";
36
+ import { summarizeGitState } from "./git-status-summary.mjs";
37
+ import { buildStatusData } from "./status-builder.mjs";
38
+ export { renderDashboard } from "./status-dashboard-renderer.mjs";
39
39
 
40
40
  export function runLegacyCheck(target) {
41
41
  const checkTarget = target.docsOnly ? target.projectRoot : target.input;
@@ -94,10 +94,10 @@ export function validateReviewSchema(target, { strict = true } = {}) {
94
94
  }
95
95
  const { header, rows } = tableAfterHeading(content, /^ID$/i);
96
96
  if (rows.length === 0) continue;
97
- const severityIndex = getColumnAny(header, ["Severity", "严重级别"]);
98
- const openIndex = getColumnAny(header, ["Open", "是否开放"]);
99
- const dispositionIndex = getColumnAny(header, ["Disposition", "处置"]);
100
- const blocksIndex = getColumnAny(header, ["Blocks Release", "是否阻塞发布"]);
97
+ const severityIndex = getColumnAny(header, reviewFindingColumns.severity);
98
+ const openIndex = getColumnAny(header, reviewFindingColumns.open);
99
+ const dispositionIndex = getColumnAny(header, reviewFindingColumns.disposition);
100
+ const blocksIndex = getColumnAny(header, reviewFindingColumns.blocksRelease);
101
101
  const followUpIndex = getColumnAny(header, ["Follow-up", "跟进"]);
102
102
  const evidenceCheckedIndex = getColumnAny(header, ["Evidence Checked", "已检查证据"]);
103
103
  if ([severityIndex, openIndex, dispositionIndex, blocksIndex].some((index) => index < 0)) {
@@ -108,9 +108,9 @@ export function validateReviewSchema(target, { strict = true } = {}) {
108
108
  const id = row[0] || "";
109
109
  const severity = row[severityIndex] || "";
110
110
  if (!/^P[0-3]$/.test(severity) && !/^(R|SR)-\d+/i.test(id)) continue;
111
- const open = (row[openIndex] || "").toLowerCase();
111
+ const open = normalizeReviewBoolean(row[openIndex] || "");
112
112
  const disposition = (row[dispositionIndex] || "").toLowerCase();
113
- const blocks = (row[blocksIndex] || "").toLowerCase();
113
+ const blocks = normalizeReviewBoolean(row[blocksIndex] || "");
114
114
  const followUp = row[followUpIndex] || "";
115
115
  if (!/^P[0-3]$/.test(severity)) report(`${relative} ${id} invalid severity: ${severity}`);
116
116
  if (!["yes", "no"].includes(open)) report(`${relative} ${id} invalid Open value: ${open}`);
@@ -128,18 +128,19 @@ export function validateReviewSchema(target, { strict = true } = {}) {
128
128
  for (const ref of refs) {
129
129
  if (ref !== "none" && /^E-\d+/i.test(ref) && !evidenceIds.has(ref)) {
130
130
  failures.push(`${relative} ${id} references missing evidence id: ${ref}`);
131
- }
132
- }
133
131
  }
134
132
  }
135
133
  }
134
+ }
135
+
136
+ }
136
137
  return { failures, warnings };
137
138
  }
138
139
 
139
- export function validateVisualMaps(target) {
140
+ export function validateVisualMaps(target, { taskPlanPaths } = {}) {
140
141
  const failures = [];
141
142
  const warnings = [];
142
- for (const taskPlanPath of listTaskPlanPaths(target)) {
143
+ for (const taskPlanPath of taskPlanPaths || listTaskPlanPaths(target)) {
143
144
  const taskDir = path.dirname(taskPlanPath);
144
145
  const visualMapPath = path.join(taskDir, visualMapFile);
145
146
  const legacyPath = path.join(taskDir, legacyVisualRoadmapFile);
@@ -153,7 +154,10 @@ export function validateVisualMaps(target) {
153
154
  }
154
155
  }
155
156
  const phases = parsePhases(visualMap.content);
157
+ const budget = parseTaskBudget(taskPlan);
156
158
  for (const phase of phases) {
159
+ if (!allowedPhaseKinds.has(phase.kind)) failures.push(`${relative} phase ${phase.id} invalid kind: ${phase.kind}`);
160
+ if (!allowedPhaseActors.has(phase.actor)) failures.push(`${relative} phase ${phase.id} invalid actor: ${phase.actor}`);
157
161
  if (!allowedPhaseStates.has(phase.state)) failures.push(`${relative} phase ${phase.id} invalid state: ${phase.state}`);
158
162
  if (!allowedEvidenceStatus.has(phase.evidenceStatus)) {
159
163
  failures.push(`${relative} phase ${phase.id} invalid evidence status: ${phase.evidenceStatus}`);
@@ -168,6 +172,9 @@ export function validateVisualMaps(target) {
168
172
  failures.push(`${relative} missing Visual Map Contract: v1.0`);
169
173
  }
170
174
  if (visualMap.source === "canonical" && phases.length === 0) warnings.push(`${relative} has no Visual Map phase table`);
175
+ if (visualMap.source === "canonical" && budget !== "simple" && phases.length > 0 && !phases.some((phase) => phase.kind === "execution" && phase.state !== "skipped")) {
176
+ failures.push(`${relative} requires at least one non-skipped execution phase`);
177
+ }
171
178
  if (visualMap.source === "legacy" && fs.existsSync(legacyPath)) {
172
179
  warnings.push(`${relative} missing; legacy visual_roadmap.md is rewrite input only`);
173
180
  } else if (visualMap.source === "legacy" && phases.length > 0) {
@@ -177,34 +184,7 @@ export function validateVisualMaps(target) {
177
184
  return { failures, warnings };
178
185
  }
179
186
 
180
- export function validatePlanContracts(target, { strict = true } = {}) {
181
- const failures = [];
182
- const warnings = [];
183
- const report = (message) => {
184
- if (strict) failures.push(message);
185
- else warnings.push(`adoption-needed: ${message}`);
186
- };
187
- for (const taskPlanPath of listTaskPlanPaths(target)) {
188
- const taskDir = path.dirname(taskPlanPath);
189
- const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
190
- const taskPlanContent = readFileSafe(taskPlanPath);
191
- const budget = parseTaskBudget(taskPlanContent);
192
- const taskContract = parseTaskContractInfo(taskPlanContent);
193
- if (!taskContract.generated) {
194
- warnings.push(`adoption-needed: ${relativeDir} missing Task Contract: harness-task/v1 marker`);
195
- }
196
- const requiredFiles = budget === "simple" ? [visualMapFile] : ["execution_strategy.md", visualMapFile, lessonCandidatesFile];
197
- for (const fileName of requiredFiles) {
198
- if (!fs.existsSync(path.join(taskDir, fileName))) {
199
- if (taskContract.generated) failures.push(`${relativeDir} missing ${fileName}`);
200
- else report(`${relativeDir} missing ${fileName}`);
201
- }
202
- }
203
- }
204
- return { failures, warnings };
205
- }
206
-
207
- export function validateTaskPresetContracts(target) {
187
+ export function validateTaskPresetContracts(target, { tasks } = {}) {
208
188
  const failures = [];
209
189
  const allowedMigrationLevels = new Set([
210
190
  "migration-baseline",
@@ -212,15 +192,40 @@ export function validateTaskPresetContracts(target) {
212
192
  "migration-full-cutover",
213
193
  "migration-deferred",
214
194
  ]);
215
- for (const task of collectTasks(target)) {
195
+ for (const task of tasks || collectTasks(target)) {
216
196
  if (!task.taskPreset || task.taskPreset === "none") continue;
197
+ let presetPackage = null;
198
+ try {
199
+ presetPackage = readPresetPackage(task.taskPreset, { targetInput: target.projectRoot });
200
+ } catch (error) {
201
+ failures.push(`${task.path} unsupported Task Preset: ${task.taskPreset} (${error.message})`);
202
+ continue;
203
+ }
204
+ if (presetPackage?.task?.kind && task.taskKind !== presetPackage.task.kind) {
205
+ failures.push(`${task.path} ${task.taskPreset} preset Task Kind mismatch: expected ${presetPackage.task.kind}, got ${task.taskKind || "(missing)"}`);
206
+ }
207
+ if (String(task.presetVersion || "") !== String(presetPackage.version)) {
208
+ failures.push(`${task.path} ${task.taskPreset} preset missing Preset Version ${presetPackage.version}`);
209
+ }
210
+ if (task.taskPreset !== "lesson-sedimentation" && (presetPackage.evidence?.bundleDir || presetPackage.audit?.evidenceFiles?.length || Object.keys(presetPackage.evidence?.files || {}).length)) {
211
+ if (!task.evidenceBundle) failures.push(`${task.path} ${task.taskPreset} preset missing Evidence Bundle`);
212
+ else if (!fs.existsSync(path.join(target.projectRoot, String(task.evidenceBundle).replace(/^TARGET:/, "").replace(/^\/+/, "")))) {
213
+ failures.push(`${task.path} ${task.taskPreset} preset Evidence Bundle missing: ${task.evidenceBundle}`);
214
+ }
215
+ }
216
+ if (task.taskPreset !== "lesson-sedimentation") {
217
+ failures.push(...validateTaskPresetAuditSnapshot(target, task, presetPackage));
218
+ }
219
+ failures.push(...validatePresetResourcesForTask(target, task, presetPackage));
220
+ if (task.taskPreset === "lesson-sedimentation") {
221
+ if (!["standard", "complex"].includes(task.budget)) failures.push(`${task.path} lesson-sedimentation preset requires Selected budget: standard or complex`);
222
+ if (!task.taskPlanPath) failures.push(`${task.path} lesson-sedimentation preset missing task plan`);
223
+ continue;
224
+ }
217
225
  if (task.taskPreset !== "legacy-migration") {
218
- failures.push(`${task.path} unsupported Task Preset: ${task.taskPreset}`);
219
226
  continue;
220
227
  }
221
228
  if (task.budget !== "complex") failures.push(`${task.path} legacy-migration preset requires Selected budget: complex`);
222
- if (!task.presetVersion) failures.push(`${task.path} legacy-migration preset missing Preset Version`);
223
- if (!task.taskKind || task.taskKind === "general") failures.push(`${task.path} legacy-migration preset missing Task Kind`);
224
229
  if (!allowedMigrationLevels.has(task.migrationTargetLevel)) {
225
230
  failures.push(`${task.path} legacy-migration preset invalid Migration Target Level: ${task.migrationTargetLevel || "(missing)"}`);
226
231
  }
@@ -228,9 +233,7 @@ export function validateTaskPresetContracts(target) {
228
233
  if (achievedLevel !== "pending" && !allowedMigrationLevels.has(achievedLevel)) {
229
234
  failures.push(`${task.path} legacy-migration preset invalid Migration Achieved Level: ${achievedLevel || "(missing)"}`);
230
235
  }
231
- if (!task.evidenceBundle) {
232
- failures.push(`${task.path} legacy-migration preset missing Evidence Bundle`);
233
- } else if (!task.migrationSnapshot?.evidencePresent) {
236
+ if (task.evidenceBundle && !task.migrationSnapshot?.evidencePresent) {
234
237
  failures.push(`${task.path} legacy-migration preset Evidence Bundle missing: ${task.evidenceBundle}`);
235
238
  } else if (!task.migrationSnapshot?.sessionPresent) {
236
239
  failures.push(`${task.path} legacy-migration preset Evidence Bundle missing session.json`);
@@ -315,196 +318,55 @@ export function validateContextDocs(target, { strict = true } = {}) {
315
318
 
316
319
  export function buildStatus(targetInput, options = {}) {
317
320
  const target = normalizeTarget(targetInput);
321
+ const gitState = summarizeGitState(target);
318
322
  const capabilityState = validateCapabilities(target);
319
323
  const declaredCapabilities = new Set(capabilityState.registry.capabilities.map((capability) => capability.name));
320
324
  const safeAdoptionMode = declaredCapabilities.has("safe-adoption");
321
325
  const shouldRunLegacy = !options.skipLegacyCheck && (capabilityState.registry.mode === "legacy-compat" || safeAdoptionMode);
322
326
  const legacy = shouldRunLegacy ? runLegacyCheck(target) : { status: "skipped", code: 0, stdout: "", stderr: "" };
323
327
  const contractStrict = Boolean(options.strict) || (capabilityState.registry.mode !== "legacy-compat" && !safeAdoptionMode);
328
+ const taskPlanPaths = listTaskPlanPaths(target);
329
+ const closeoutContent = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
330
+ const tasks = collectTasks(target, { requireGeneratedScaffoldProvenance: contractStrict, taskPlanPaths, closeoutContent });
324
331
  const reviews = validateReviewSchema(target, { strict: contractStrict });
325
- const visualMaps = validateVisualMaps(target);
326
- const planContracts = validatePlanContracts(target, { strict: contractStrict });
327
- const presetContracts = validateTaskPresetContracts(target);
332
+ const visualMaps = validateVisualMaps(target, { taskPlanPaths });
333
+ const planContracts = validatePlanContracts(target, { strict: contractStrict, taskPlanPaths });
334
+ const presetContracts = validateTaskPresetContracts(target, { tasks });
328
335
  const contextDocs = validateContextDocs(target, { strict: contractStrict });
329
- const failures = [...capabilityState.failures, ...reviews.failures, ...visualMaps.failures, ...planContracts.failures, ...presetContracts.failures, ...contextDocs.failures];
330
- const warnings = [...capabilityState.warnings, ...reviews.warnings, ...visualMaps.warnings, ...planContracts.warnings, ...presetContracts.warnings, ...contextDocs.warnings];
336
+ const governanceBoundaries = validateGovernanceTableBoundaries(target);
337
+ const subagentAuthorization = validateSubagentAuthorization(target, { strict: contractStrict });
338
+ const failures = [...capabilityState.failures, ...reviews.failures, ...visualMaps.failures, ...planContracts.failures, ...presetContracts.failures, ...contextDocs.failures, ...governanceBoundaries.failures, ...subagentAuthorization.failures];
339
+ const warnings = [...capabilityState.warnings, ...reviews.warnings, ...visualMaps.warnings, ...planContracts.warnings, ...presetContracts.warnings, ...contextDocs.warnings, ...governanceBoundaries.warnings, ...subagentAuthorization.warnings, ...gitState.warnings];
331
340
  if (legacy.status === "fail") {
332
341
  if (options.strictLegacy) failures.push("legacy check failed");
333
342
  else warnings.push(`adoption-needed: legacy check failed: ${(legacy.stderr || legacy.stdout).trim()}`);
334
343
  }
335
344
 
336
- const tasks = collectTasks(target);
345
+ const taskCompletionConsistency = validateTaskCompletionConsistency(tasks);
346
+ failures.push(...taskCompletionConsistency.failures);
347
+ warnings.push(...taskCompletionConsistency.warnings);
337
348
  const briefReady = tasks.filter((task) => task.briefSource === "standalone").length;
338
349
  const briefMissing = tasks.length - briefReady;
339
350
  for (const task of tasks) {
351
+ for (const issue of task.materialIssues || []) {
352
+ if (!String(issue.code || "").startsWith("missing-task-audit") && !String(issue.code || "").startsWith("legacy-")) continue;
353
+ const message = `${String(issue.sourcePath || task.path).replace(/^TARGET:/, "")} ${issue.message}`;
354
+ if (contractStrict || options.strictLegacy) failures.push(message);
355
+ else warnings.push(`adoption-needed: ${message}`);
356
+ }
340
357
  if (task.stateSource === "invalid") {
341
358
  const message = `${task.path}/progress.md invalid task state: ${task.stateRaw}`;
342
359
  if (contractStrict || options.strictLegacy) failures.push(message);
343
360
  else warnings.push(`adoption-needed: ${message}`);
344
361
  }
345
362
  }
346
- const capabilityNames = new Map(capabilityState.registry.capabilities.map((capability) => [capability.name, capability]));
347
- for (const detected of capabilityState.detected) {
348
- if (!capabilityNames.has(detected)) capabilityNames.set(detected, { name: detected, state: "configured" });
349
- }
350
- const cutoverCounters = taskCutoverCounters(tasks);
351
- const fullCutoverEligible =
352
- failures.length === 0 &&
353
- warnings.length === 0 &&
354
- cutoverCounters.legacyVisualOnlyCount === 0 &&
355
- cutoverCounters.unknownClassificationCount === 0 &&
356
- cutoverCounters.weakBriefCount === 0 &&
357
- cutoverCounters.missingCanonicalVisualMapCount === 0;
358
-
359
- return {
360
- project: {
361
- name: path.basename(target.projectRoot),
362
- root: `TARGET:${target.docsOnly ? toPosix(path.relative(target.projectRoot, target.docsRoot)) : "."}`,
363
- docsOnly: target.docsOnly,
364
- },
365
- schemaVersion: 2,
366
- generatedAt: new Date().toISOString(),
367
- mode: capabilityState.registry.mode,
368
- checkState: {
369
- status: failures.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass",
370
- failures: failures.length,
371
- warnings: warnings.length,
372
- details: { failures, warnings },
373
- legacy,
374
- },
375
- summary: {
376
- tasks: tasks.length,
377
- briefCoverage: {
378
- ready: briefReady,
379
- missing: briefMissing,
380
- total: tasks.length,
381
- },
382
- visualMapCoverage: {
383
- canonical: tasks.filter((task) => task.visualMapSource === "canonical").length,
384
- legacyOnly: cutoverCounters.legacyVisualOnlyCount,
385
- missing: tasks.filter((task) => task.visualMapStatus === "missing").length,
386
- total: tasks.length,
387
- },
388
- fullCutoverEligible,
389
- legacyVisualOnlyCount: cutoverCounters.legacyVisualOnlyCount,
390
- unknownClassificationCount: cutoverCounters.unknownClassificationCount,
391
- weakBriefCount: cutoverCounters.weakBriefCount,
392
- visualMapRequiredCount: cutoverCounters.visualMapRequiredCount,
393
- missingCanonicalVisualMapCount: cutoverCounters.missingCanonicalVisualMapCount,
394
- },
395
- capabilities: [...capabilityNames.values()].map((capability) => ({
396
- name: capability.name,
397
- state: capability.state || "configured",
398
- dependencyStatus: capabilityDefinitions[capability.name]?.dependencies.every((dependency) => capabilityNames.has(dependency))
399
- ? "valid"
400
- : "invalid",
401
- warnings: capabilityState.warnings.filter((warning) => warning.includes(capability.name)),
402
- })),
363
+ return buildStatusData(target, {
364
+ capabilityState,
365
+ gitState,
366
+ legacy,
367
+ failures,
368
+ warnings,
403
369
  tasks,
404
- handoffs: tasks.flatMap((task) => task.handoffs || []),
405
- recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
406
- };
407
- }
408
-
409
- export function renderDashboard(status) {
410
- const taskCards = status.tasks
411
- .map((task) => {
412
- const phases = task.phases
413
- .map(
414
- (phase) => `<div class="phase ${escapeHtml(phase.state)}">
415
- <div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${phase.completion}%</span></div>
416
- <div class="phase-output">${escapeHtml(phase.output)}</div>
417
- <div class="meter"><i style="width:${phase.completion}%"></i></div>
418
- <div class="muted">${escapeHtml(phase.state)} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
419
- </div>`,
420
- )
421
- .join("");
422
- const risks = task.risks
423
- .map((risk) => `<span class="risk ${risk.open || risk.blocksRelease ? "open" : ""}">${escapeHtml(risk.severity)} ${escapeHtml(risk.summary)}</span>`)
424
- .join("");
425
- const evidence = task.evidence
426
- .map((item) => `<span class="evidence">${escapeHtml(item.type)} · ${escapeHtml(item.summary)}</span>`)
427
- .join("");
428
- const evidenceMeter = evidenceCompletion(task.phases);
429
- return `<section class="task">
430
- <div class="task-head">
431
- <div><h2>${escapeHtml(task.title)}</h2><p>${escapeHtml(task.path)}</p></div>
432
- <div class="score">${task.completion}%</div>
433
- </div>
434
- <div class="meter"><i style="width:${task.completion}%"></i></div>
435
- <div class="phases">${phases || '<div class="empty">No phase table</div>'}</div>
436
- <div class="evidence-row"><strong>Evidence</strong><div class="meter small"><i style="width:${evidenceMeter}%"></i></div>${evidence || '<span class="empty">No evidence</span>'}</div>
437
- <div class="risks">${risks || '<span class="ok">No open visual risk</span>'}</div>
438
- </section>`;
439
- })
440
- .join("");
441
- const chips = status.capabilities
442
- .map((capability) => `<span class="chip ${escapeHtml(capability.state)}">${escapeHtml(capability.name)} · ${escapeHtml(capability.state)}</span>`)
443
- .join("");
444
- const failures = status.checkState.details.failures.map((failure) => `<li>${escapeHtml(failure)}</li>`).join("");
445
- const warnings = status.checkState.details.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("");
446
- const handoffs = status.handoffs
447
- .map((handoff) => `<span class="handoff">${escapeHtml(handoff.state)} · ${escapeHtml(handoff.summary)}</span>`)
448
- .join("");
449
- const activity = status.recentActivity
450
- .map((item) => `<li><strong>${escapeHtml(item.type)}</strong> ${escapeHtml(item.summary)}</li>`)
451
- .join("");
452
- return `<!doctype html>
453
- <html lang="zh-CN">
454
- <head>
455
- <meta charset="utf-8">
456
- <meta name="viewport" content="width=device-width, initial-scale=1">
457
- <title>${escapeHtml(status.project.name)} Harness Dashboard</title>
458
- <style>
459
- :root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#17202a;background:#f6f7f9}
460
- body{margin:0}.shell{max-width:1180px;margin:0 auto;padding:28px}
461
- header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}
462
- h1,h2{margin:0;letter-spacing:0}h1{font-size:30px}h2{font-size:18px}p{margin:6px 0;color:#687382}
463
- .pill,.chip,.risk,.ok{display:inline-flex;align-items:center;border-radius:999px;padding:6px 10px;font-size:12px;margin:4px;background:#e8edf3;color:#273444}
464
- .pass,.verified{background:#dff5e8;color:#125c32}.warn,.configured{background:#fff0cc;color:#765100}.fail,.open{background:#ffe1df;color:#8a1c12}.scaffolded{background:#e8edf3;color:#273444}
465
- .grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}.stat,.task{background:#fff;border:1px solid #e4e8ee;border-radius:8px;padding:16px}
466
- .stat strong{font-size:24px;display:block}.capabilities{margin-bottom:20px}.task{margin-bottom:16px}.task-head{display:flex;justify-content:space-between;gap:16px}
467
- .score{font-size:28px;font-weight:700;color:#223047}.meter{height:8px;background:#edf1f5;border-radius:99px;overflow:hidden;margin:10px 0}.meter i{display:block;height:100%;background:#2f6fed}.meter.small{height:6px;max-width:180px}
468
- .evidence,.handoff{display:inline-flex;padding:5px 8px;margin:4px;border-radius:6px;background:#edf7ff;color:#214d72;font-size:12px}.handoff{background:#fff3d8;color:#745000}
469
- .phases{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:12px}.phase{border:1px solid #e5eaf0;border-radius:8px;padding:12px;background:#fbfcfe}.phase-top{display:flex;justify-content:space-between}.phase-output{min-height:38px;margin-top:8px}
470
- .risks{margin-top:12px}.empty{color:#8a95a3}.panel{background:#fff;border:1px solid #e4e8ee;border-radius:8px;padding:16px;margin-top:16px}
471
- @media(max-width:760px){.shell{padding:16px}header{display:block}.grid{grid-template-columns:1fr 1fr}.task-head{display:block}}
472
- </style>
473
- </head>
474
- <body><main class="shell">
475
- <header>
476
- <div><h1>${escapeHtml(status.project.name)} Harness Dashboard</h1><p>${escapeHtml(status.project.root)} · ${escapeHtml(status.generatedAt)}</p></div>
477
- <span class="pill ${escapeHtml(status.checkState.status)}">${escapeHtml(status.checkState.status)} · ${escapeHtml(status.mode)}</span>
478
- </header>
479
- <section class="grid">
480
- <div class="stat"><strong>${status.tasks.length}</strong><span>Tasks</span></div>
481
- <div class="stat"><strong>${status.capabilities.length}</strong><span>Capabilities</span></div>
482
- <div class="stat"><strong>${status.checkState.failures}</strong><span>Failures</span></div>
483
- <div class="stat"><strong>${status.checkState.warnings}</strong><span>Warnings</span></div>
484
- </section>
485
- <section class="capabilities">${chips}</section>
486
- <section class="panel"><h2>Handoffs</h2>${handoffs || '<span class="ok">No pending handoff</span>'}</section>
487
- ${taskCards || '<section class="task">No tasks found.</section>'}
488
- <section class="panel"><h2>Recent Activity</h2><ul>${activity || "<li>None</li>"}</ul></section>
489
- <section class="panel"><h2>Failures</h2><ul>${failures || "<li>None</li>"}</ul><h2>Warnings</h2><ul>${warnings || "<li>None</li>"}</ul></section>
490
- </main></body></html>`;
491
- }
492
-
493
- function escapeHtml(value) {
494
- return String(value ?? "")
495
- .replaceAll("&", "&amp;")
496
- .replaceAll("<", "&lt;")
497
- .replaceAll(">", "&gt;")
498
- .replaceAll('"', "&quot;");
499
- }
500
-
501
- function evidenceCompletion(phases) {
502
- const scored = phases.filter((phase) => phase.state !== "skipped");
503
- if (scored.length === 0) return 0;
504
- const score = scored.reduce((sum, phase) => {
505
- if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
506
- if (phase.evidenceStatus === "partial") return sum + 50;
507
- return sum;
508
- }, 0);
509
- return Math.round(score / scored.length);
370
+ validationMode: "validated",
371
+ });
510
372
  }