coding-agent-harness 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.en-US.md +14 -0
  4. package/README.md +230 -80
  5. package/README.zh-CN.md +290 -0
  6. package/SKILL.md +132 -198
  7. package/docs-release/README.md +80 -9
  8. package/docs-release/architecture/overview.md +298 -28
  9. package/docs-release/architecture/overview.zh-CN.md +292 -0
  10. package/docs-release/assets/dashboard-overview.png +0 -0
  11. package/docs-release/assets/harness-architecture.svg +163 -0
  12. package/docs-release/assets/harness-workflow.svg +64 -0
  13. package/docs-release/guides/agent-installation.en-US.md +237 -0
  14. package/docs-release/guides/agent-installation.md +149 -27
  15. package/docs-release/guides/contributing.md +100 -0
  16. package/docs-release/guides/contributing.zh-CN.md +99 -0
  17. package/docs-release/guides/document-audience-and-surfaces.en-US.md +113 -0
  18. package/docs-release/guides/document-audience-and-surfaces.md +113 -0
  19. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
  20. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
  21. package/docs-release/guides/legacy-migration-agent-prompt.md +373 -0
  22. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +350 -0
  23. package/docs-release/guides/migration-playbook.en-US.md +324 -0
  24. package/docs-release/guides/migration-playbook.md +328 -0
  25. package/docs-release/guides/parent-control-repository-pattern.en-US.md +254 -0
  26. package/docs-release/guides/parent-control-repository-pattern.md +254 -0
  27. package/docs-release/guides/preset-development.md +214 -0
  28. package/docs-release/guides/repository-operating-models.en-US.md +197 -0
  29. package/docs-release/guides/repository-operating-models.md +197 -0
  30. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  31. package/docs-release/guides/task-state-machine.md +214 -0
  32. package/docs-release/intl/README.md +15 -0
  33. package/docs-release/intl/de-DE.md +18 -0
  34. package/docs-release/intl/en-US.md +18 -0
  35. package/docs-release/intl/es-ES.md +18 -0
  36. package/docs-release/intl/fr-FR.md +18 -0
  37. package/docs-release/intl/ja-JP.md +18 -0
  38. package/docs-release/intl/ko-KR.md +18 -0
  39. package/docs-release/intl/zh-CN.md +18 -0
  40. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
  41. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  42. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
  43. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
  44. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
  45. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
  46. package/package.json +10 -3
  47. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  48. package/presets/legacy-migration/preset.yaml +134 -0
  49. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  50. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  51. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  52. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  53. package/presets/legacy-migration/templates/review.seed.md +12 -0
  54. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  55. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  56. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  57. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  58. package/presets/lesson-sedimentation/preset.yaml +23 -0
  59. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  60. package/presets/module/preset.yaml +25 -0
  61. package/presets/module/templates/execution_strategy.append.md +8 -0
  62. package/presets/module/templates/task_plan.append.md +17 -0
  63. package/presets/standard-task/preset.yaml +31 -0
  64. package/presets/standard-task/templates/task_plan.append.md +7 -0
  65. package/references/adversarial-review-standard.md +2 -2
  66. package/references/agents-md-pattern.md +5 -5
  67. package/references/delivery-operating-model-standard.md +3 -3
  68. package/references/docs-directory-standard.md +53 -10
  69. package/references/external-source-intake-standard.md +75 -0
  70. package/references/harness-ledger.md +53 -94
  71. package/references/legacy-12-phase-bootstrap.md +41 -0
  72. package/references/lessons-governance.md +100 -88
  73. package/references/module-parallel-standard.md +14 -14
  74. package/references/planning-loop.md +51 -7
  75. package/references/project-onboarding-audit.md +10 -0
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +12 -1
  78. package/references/review-routing-standard.md +7 -1
  79. package/references/ssot-governance.md +67 -59
  80. package/references/taskr-gap-analysis.md +600 -0
  81. package/references/testing-standard.md +50 -0
  82. package/references/walkthrough-closeout.md +10 -9
  83. package/scripts/check-harness.mjs +111 -331
  84. package/scripts/commands/dashboard-command.mjs +67 -0
  85. package/scripts/commands/migration-command.mjs +96 -0
  86. package/scripts/commands/preset-command.mjs +73 -0
  87. package/scripts/commands/task-command.mjs +327 -0
  88. package/scripts/harness.mjs +106 -20
  89. package/scripts/lib/capability-registry.mjs +591 -0
  90. package/scripts/lib/check-module-parallel.mjs +237 -0
  91. package/scripts/lib/check-profiles.mjs +418 -0
  92. package/scripts/lib/check-task-contracts.mjs +47 -0
  93. package/scripts/lib/core-shared.mjs +196 -0
  94. package/scripts/lib/dashboard-data.mjs +412 -0
  95. package/scripts/lib/dashboard-workbench.mjs +257 -0
  96. package/scripts/lib/dashboard-writer.mjs +107 -4
  97. package/scripts/lib/git-status-summary.mjs +46 -0
  98. package/scripts/lib/governance-index-generator.mjs +174 -0
  99. package/scripts/lib/governance-sync.mjs +514 -0
  100. package/scripts/lib/governance-table-boundary.mjs +175 -0
  101. package/scripts/lib/harness-core.mjs +15 -1318
  102. package/scripts/lib/lesson-maintenance.mjs +152 -0
  103. package/scripts/lib/markdown-utils.mjs +158 -0
  104. package/scripts/lib/migration-planner.mjs +478 -0
  105. package/scripts/lib/migration-support.mjs +312 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +497 -0
  108. package/scripts/lib/preset-registry.mjs +627 -0
  109. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  110. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  111. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  112. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  113. package/scripts/lib/task-completion-consistency.mjs +16 -0
  114. package/scripts/lib/task-index.mjs +93 -0
  115. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  116. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  117. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  118. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  119. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  120. package/scripts/lib/task-lifecycle.mjs +649 -0
  121. package/scripts/lib/task-review-model.mjs +469 -0
  122. package/scripts/lib/task-scanner.mjs +576 -0
  123. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  124. package/scripts/postinstall.mjs +14 -0
  125. package/skills/preset-creator/SKILL.md +179 -0
  126. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  127. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  128. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  129. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  130. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  131. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  132. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  133. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  134. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  137. package/{templates/planning/visual_roadmap.md → skills/preset-creator/references/complex-task-skeleton/visual_map.md} +24 -2
  138. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  139. package/templates/AGENTS.md.template +51 -36
  140. package/templates/architecture/Architecture-SSoT.md +21 -0
  141. package/templates/architecture/README.md +49 -0
  142. package/templates/architecture/critical-flows.md +22 -0
  143. package/templates/architecture/local-repo-context.md +20 -0
  144. package/templates/architecture/service-catalog.md +17 -0
  145. package/templates/architecture/services/service-template.md +31 -0
  146. package/templates/architecture/system-map.md +22 -0
  147. package/templates/dashboard/assets/app-src/00-state.js +42 -0
  148. package/templates/dashboard/assets/app-src/10-router.js +77 -0
  149. package/templates/dashboard/assets/app-src/20-overview.js +241 -0
  150. package/templates/dashboard/assets/app-src/30-tasks.js +409 -0
  151. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  152. package/templates/dashboard/assets/app-src/40-modules.js +58 -0
  153. package/templates/dashboard/assets/app-src/45-review.js +347 -0
  154. package/templates/dashboard/assets/app-src/50-migration.js +183 -0
  155. package/templates/dashboard/assets/app-src/60-shared.js +61 -0
  156. package/templates/dashboard/assets/app-src/90-bindings.js +524 -0
  157. package/templates/dashboard/assets/app.css +3107 -300
  158. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  159. package/templates/dashboard/assets/app.js +2068 -306
  160. package/templates/dashboard/assets/app.manifest.json +12 -0
  161. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  162. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  163. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  164. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  165. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  166. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  167. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  168. package/templates/dashboard/assets/i18n.js +531 -44
  169. package/templates/dashboard/assets/mermaid-renderer.js +58 -8
  170. package/templates/development/README.md +52 -0
  171. package/templates/development/codebase-map.md +11 -0
  172. package/templates/development/cross-repo-debugging.md +18 -0
  173. package/templates/development/external-context/service-template.md +33 -0
  174. package/templates/development/external-source-packs/README.md +24 -0
  175. package/templates/development/external-source-packs/digest-template.md +28 -0
  176. package/templates/development/local-setup.md +16 -0
  177. package/templates/development/stubs-and-mocks.md +11 -0
  178. package/templates/integrations/README.md +40 -0
  179. package/templates/integrations/api-contract.md +42 -0
  180. package/templates/integrations/event-contract.md +46 -0
  181. package/templates/integrations/third-party/vendor-template.md +42 -0
  182. package/templates/integrations/webhook-contract.md +41 -0
  183. package/templates/ledger/Harness-Ledger.md +13 -25
  184. package/templates/lessons/lesson-arch-process-change.md +1 -1
  185. package/templates/lessons/lesson-new-doc.md +1 -1
  186. package/templates/lessons/lesson-ref-change.md +1 -1
  187. package/templates/planning/brief.md +32 -0
  188. package/templates/planning/execution_strategy.md +31 -0
  189. package/templates/planning/lesson_candidates.md +70 -0
  190. package/templates/planning/long-running-task-contract.md +7 -0
  191. package/templates/planning/module_brief.md +25 -0
  192. package/templates/planning/module_session_prompt.md +6 -0
  193. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  194. package/templates/planning/optional/references/INDEX.md +3 -3
  195. package/templates/planning/review.md +59 -0
  196. package/templates/planning/task_plan.md +40 -15
  197. package/templates/planning/visual_map.md +50 -0
  198. package/templates/reference/docs-library-standard.md +31 -0
  199. package/templates/reference/execution-workflow-standard.md +5 -2
  200. package/templates/reference/external-source-intake-standard.md +82 -0
  201. package/templates/reference/harness-ledger-standard.md +1 -0
  202. package/templates/reference/pull-request-standard.md +80 -0
  203. package/templates/reference/repo-governance-standard.md +8 -5
  204. package/templates/reference/review-routing-standard.md +6 -0
  205. package/templates/reference/walkthrough-standard.md +3 -1
  206. package/templates/verifier/verifier-output.md +1 -1
  207. package/templates/walkthrough/walkthrough-template.md +2 -2
  208. package/templates-zh-CN/AGENTS.md.template +73 -70
  209. package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
  210. package/templates-zh-CN/architecture/README.md +51 -0
  211. package/templates-zh-CN/architecture/critical-flows.md +24 -0
  212. package/templates-zh-CN/architecture/local-repo-context.md +20 -0
  213. package/templates-zh-CN/architecture/service-catalog.md +17 -0
  214. package/templates-zh-CN/architecture/services/service-template.md +31 -0
  215. package/templates-zh-CN/architecture/system-map.md +22 -0
  216. package/templates-zh-CN/development/README.md +54 -0
  217. package/templates-zh-CN/development/codebase-map.md +11 -0
  218. package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
  219. package/templates-zh-CN/development/external-context/service-template.md +33 -0
  220. package/templates-zh-CN/development/external-source-packs/README.md +24 -0
  221. package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
  222. package/templates-zh-CN/development/local-setup.md +16 -0
  223. package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
  224. package/templates-zh-CN/integrations/README.md +42 -0
  225. package/templates-zh-CN/integrations/api-contract.md +42 -0
  226. package/templates-zh-CN/integrations/event-contract.md +46 -0
  227. package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
  228. package/templates-zh-CN/integrations/webhook-contract.md +41 -0
  229. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  230. package/templates-zh-CN/planning/brief.md +32 -0
  231. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  232. package/templates-zh-CN/planning/lesson_candidates.md +70 -0
  233. package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
  234. package/templates-zh-CN/planning/module_brief.md +25 -0
  235. package/templates-zh-CN/planning/module_plan.md +2 -2
  236. package/templates-zh-CN/planning/module_session_prompt.md +4 -3
  237. package/templates-zh-CN/planning/review.md +59 -1
  238. package/templates-zh-CN/planning/task_plan.md +37 -11
  239. package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
  240. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  241. package/templates-zh-CN/reference/docs-library-standard.md +36 -1
  242. package/templates-zh-CN/reference/execution-workflow-standard.md +10 -2
  243. package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
  244. package/templates-zh-CN/reference/harness-ledger-standard.md +7 -4
  245. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  246. package/templates-zh-CN/reference/repo-governance-standard.md +4 -1
  247. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  248. package/templates-zh-CN/reference/walkthrough-standard.md +6 -5
  249. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
  250. package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
  251. package/scripts/smoke-dashboard.mjs +0 -70
  252. package/scripts/test-harness.mjs +0 -483
  253. package/templates/ssot/Feature-SSoT.md +0 -43
  254. package/templates/ssot/Lessons-SSoT.md +0 -44
  255. package/templates-zh-CN/dashboard/assets/app.css +0 -399
  256. package/templates-zh-CN/dashboard/assets/app.js +0 -435
  257. package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
  258. package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
  259. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
  260. package/templates-zh-CN/dashboard/index.html +0 -18
  261. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  262. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -1,1318 +1,15 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { writeDashboardDirectory } from "./dashboard-writer.mjs";
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const repoRoot = path.resolve(__dirname, "../..");
10
- const legacyChecker = path.join(repoRoot, "scripts/check-harness.mjs");
11
-
12
- export const capabilityDefinitions = {
13
- core: {
14
- description: "Planning loop and task execution records.",
15
- selectWhen: "Always install. This is the required document kernel.",
16
- default: true,
17
- dependencies: [],
18
- artifacts: ["docs/09-PLANNING"],
19
- },
20
- "module-parallel": {
21
- description: "Module registry, module plans, session prompts, and worker handoff.",
22
- selectWhen: "Use only when the project has two or more independent modules that need parallel ownership.",
23
- default: false,
24
- dependencies: ["core"],
25
- artifacts: ["docs/09-PLANNING/Module-Registry.md", "docs/09-PLANNING/MODULES"],
26
- },
27
- "subagent-worker": {
28
- description: "Commit-backed worker handoff protocol for code-changing subagents.",
29
- selectWhen: "Use only when code-changing subagents will work in dedicated worktrees with commit-backed handoff.",
30
- default: false,
31
- dependencies: ["module-parallel"],
32
- artifacts: ["docs/09-PLANNING/MODULES"],
33
- },
34
- "adversarial-review": {
35
- description: "Machine-gateable adversarial review reports and verifier output contract.",
36
- selectWhen: "Use when release, architecture, security, data, or strategy risk requires an independent review artifact.",
37
- default: false,
38
- dependencies: ["core"],
39
- artifacts: ["docs/09-PLANNING/TASKS"],
40
- },
41
- "long-running-task": {
42
- description: "Long-running task contract with review cadence and stop conditions.",
43
- selectWhen: "Use when agents may run across many loops without user confirmation after every step.",
44
- default: false,
45
- dependencies: ["core"],
46
- artifacts: ["docs/09-PLANNING/TASKS/_task-template/long-running-task-contract.md"],
47
- },
48
- "dashboard": {
49
- description: "Read-only HTML dashboard generated from harness status JSON.",
50
- selectWhen: "Use when users or agents need a local read-only status surface.",
51
- default: false,
52
- dependencies: ["core"],
53
- artifacts: [],
54
- },
55
- "safe-adoption": {
56
- description: "Legacy compatibility and assisted capability adoption.",
57
- selectWhen: "Use when adopting v1.0 into an existing harness project without rewriting history.",
58
- default: false,
59
- dependencies: ["core"],
60
- artifacts: [],
61
- },
62
- };
63
-
64
- export const capabilityAliases = {
65
- "review-contract": "adversarial-review",
66
- };
67
-
68
- export const supportedLocales = new Set(["zh-CN", "en-US"]);
69
- export const allowedCapabilityStates = new Set(["scaffolded", "configured", "verified"]);
70
- export const userInstallTargets = {
71
- codex: [".codex", "skills", "coding-agent-harness"],
72
- claude: [".claude", "skills", "coding-agent-harness"],
73
- gemini: [".gemini", "skills", "coding-agent-harness"],
74
- openclaw: [".openclaw", "skills", "coding-agent-harness"],
75
- agents: [".agents", "skills", "coding-agent-harness"],
76
- };
77
- export const allowedReviewDispositions = new Set([
78
- "open",
79
- "mitigated",
80
- "closed",
81
- "deferred",
82
- "accepted-risk",
83
- "not-reproducible",
84
- "out-of-scope",
85
- ]);
86
- export const allowedPhaseStates = new Set(["planned", "in_progress", "review", "blocked", "done", "skipped"]);
87
- export const allowedEvidenceStatus = new Set(["missing", "partial", "present", "waived"]);
88
-
89
- export function normalizeTarget(input = ".") {
90
- const target = path.resolve(input);
91
- const isDocsRoot =
92
- path.basename(target) === "docs" &&
93
- (fs.existsSync(path.join(target, "09-PLANNING")) || fs.existsSync(path.join(target, "11-REFERENCE")));
94
- return {
95
- input: target,
96
- projectRoot: isDocsRoot ? path.dirname(target) : target,
97
- docsRoot: isDocsRoot ? target : path.join(target, "docs"),
98
- docsOnly: isDocsRoot,
99
- };
100
- }
101
-
102
- export function toPosix(value) {
103
- return value.split(path.sep).join("/");
104
- }
105
-
106
- export function exists(target, relativePath) {
107
- return fs.existsSync(path.join(target.projectRoot, relativePath));
108
- }
109
-
110
- export function existsInDocs(target, relativePath) {
111
- return fs.existsSync(path.join(target.docsRoot, relativePath));
112
- }
113
-
114
- export function readFileSafe(filePath) {
115
- try {
116
- return fs.readFileSync(filePath, "utf8");
117
- } catch {
118
- return "";
119
- }
120
- }
121
-
122
- function walkFiles(root) {
123
- const results = [];
124
- if (!fs.existsSync(root)) return results;
125
- function walk(dir) {
126
- for (const entry of fs.readdirSync(dir)) {
127
- const full = path.join(dir, entry);
128
- const stat = fs.statSync(full);
129
- if (stat.isDirectory()) {
130
- if ([".git", "node_modules", "tmp"].includes(entry)) continue;
131
- walk(full);
132
- } else {
133
- results.push(full);
134
- }
135
- }
136
- }
137
- walk(root);
138
- return results;
139
- }
140
-
141
- export function readCapabilityRegistry(target) {
142
- const registryPath = path.join(target.projectRoot, ".harness-capabilities.json");
143
- if (!fs.existsSync(registryPath)) {
144
- return {
145
- mode: "legacy-compat",
146
- path: registryPath,
147
- capabilities: [{ name: "core", state: "configured" }],
148
- locale: "en-US",
149
- raw: null,
150
- errors: [],
151
- };
152
- }
153
-
154
- try {
155
- const raw = JSON.parse(fs.readFileSync(registryPath, "utf8"));
156
- const locale = normalizeLocale(raw.locale);
157
- const capabilities = Array.isArray(raw.capabilities)
158
- ? raw.capabilities.map((entry) =>
159
- typeof entry === "string"
160
- ? { name: normalizeCapabilityName(entry), state: "scaffolded" }
161
- : { name: normalizeCapabilityName(entry.name), state: entry.state || "scaffolded" },
162
- )
163
- : [];
164
- return { mode: "declared-capability", path: registryPath, capabilities, raw, locale, errors: [] };
165
- } catch (error) {
166
- return { mode: "declared-capability", path: registryPath, capabilities: [], raw: null, errors: [error.message] };
167
- }
168
- }
169
-
170
- function normalizeCapabilityName(name) {
171
- return capabilityAliases[name] || name;
172
- }
173
-
174
- export function normalizeLocale(locale = "en-US") {
175
- return supportedLocales.has(locale) ? locale : "en-US";
176
- }
177
-
178
- export function detectCapabilities(target) {
179
- const detected = new Set(["core"]);
180
- if (existsInDocs(target, "09-PLANNING/Module-Registry.md")) detected.add("module-parallel");
181
- if (existsInDocs(target, "11-REFERENCE/adversarial-review-standard.md")) detected.add("adversarial-review");
182
- if (
183
- existsInDocs(target, "11-REFERENCE/long-running-task-standard.md") ||
184
- existsInDocs(target, "09-PLANNING/TASKS/_task-template/long-running-task-contract.md")
185
- ) {
186
- detected.add("long-running-task");
187
- }
188
- return [...detected];
189
- }
190
-
191
- export function buildInstallReport({ target, locale, capabilities, changes, dryRun = false, operation = "init" }) {
192
- const selected = new Set(capabilities.map(normalizeCapabilityName));
193
- return {
194
- operation,
195
- dryRun,
196
- target: target.projectRoot,
197
- locale,
198
- capabilities: Object.entries(capabilityDefinitions).map(([name, definition]) => ({
199
- name,
200
- selected: selected.has(name),
201
- default: definition.default === true,
202
- dependencies: definition.dependencies,
203
- description: definition.description,
204
- selectWhen: definition.selectWhen,
205
- })),
206
- selectedCapabilities: capabilities,
207
- created: changes.filter((change) => ["create", "would-create"].includes(change.action)).map((change) => change.destination),
208
- skipped: changes.filter((change) => change.action === "skip-existing").map((change) => change.destination),
209
- agentInstructions: [
210
- "Agents must choose locale during Decide and pass --locale zh-CN|en-US explicitly in non-interactive installs.",
211
- "Use core for every install; add optional capabilities only when their selectWhen rule is true.",
212
- "After scaffold, run Configure before marking capabilities configured or verified.",
213
- "Run harness check/status/dashboard and record residuals before delivery.",
214
- ],
215
- verificationCommands: [
216
- `node scripts/harness.mjs check --profile target-project ${target.projectRoot}`,
217
- `node scripts/harness.mjs status --json ${target.projectRoot}`,
218
- `node scripts/harness.mjs dashboard --out /tmp/harness-dashboard.html ${target.projectRoot}`,
219
- ],
220
- };
221
- }
222
-
223
- function packageVersion() {
224
- try {
225
- const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"));
226
- return pkg.version || "";
227
- } catch {
228
- return "";
229
- }
230
- }
231
-
232
- function userHome(home = "") {
233
- return path.resolve(home || os.homedir());
234
- }
235
-
236
- function normalizeUserAgent(agent = "codex") {
237
- const normalized = String(agent || "codex").toLowerCase();
238
- if (normalized === "all") return Object.keys(userInstallTargets);
239
- if (!userInstallTargets[normalized]) throw new Error(`Unknown user agent target: ${agent}`);
240
- return [normalized];
241
- }
242
-
243
- function targetForUserAgent(agent, home = "") {
244
- return path.join(userHome(home), ...userInstallTargets[agent]);
245
- }
246
-
247
- function skillPackageEntries() {
248
- return [
249
- "README.md",
250
- "CHANGELOG.md",
251
- "SKILL.md",
252
- "LICENSE",
253
- "package.json",
254
- "references",
255
- "templates",
256
- "templates-zh-CN",
257
- "scripts",
258
- "docs-release",
259
- "examples",
260
- ];
261
- }
262
-
263
- function listPackageFiles() {
264
- const files = [];
265
- function walk(relativePath) {
266
- const full = path.join(repoRoot, relativePath);
267
- if (!fs.existsSync(full)) return;
268
- const stat = fs.statSync(full);
269
- if (stat.isDirectory()) {
270
- for (const entry of fs.readdirSync(full)) walk(path.join(relativePath, entry));
271
- return;
272
- }
273
- if (stat.isFile()) files.push(toPosix(relativePath));
274
- }
275
- for (const entry of skillPackageEntries()) walk(entry);
276
- return files.sort();
277
- }
278
-
279
- function copySkillPackage(targetRoot, { dryRun = false, force = false } = {}) {
280
- const changes = [];
281
- for (const relativeFile of listPackageFiles()) {
282
- const source = path.join(repoRoot, relativeFile);
283
- const destination = path.join(targetRoot, relativeFile);
284
- const existsAlready = fs.existsSync(destination);
285
- const action = existsAlready ? (force ? "overwrite" : "skip-existing") : dryRun ? "would-create" : "create";
286
- changes.push({ source: relativeFile, destination, action });
287
- if (dryRun || (existsAlready && !force)) continue;
288
- fs.mkdirSync(path.dirname(destination), { recursive: true });
289
- fs.copyFileSync(source, destination);
290
- }
291
- return changes;
292
- }
293
-
294
- export function installUserSkill({ agent = "codex", home = "", dryRun = false, force = false } = {}) {
295
- const agents = normalizeUserAgent(agent);
296
- const targets = agents.map((targetAgent) => {
297
- const target = targetForUserAgent(targetAgent, home);
298
- const changes = copySkillPackage(target, { dryRun, force });
299
- return {
300
- agent: targetAgent,
301
- target,
302
- changes,
303
- created: changes.filter((change) => ["create", "would-create"].includes(change.action)).length,
304
- overwritten: changes.filter((change) => change.action === "overwrite").length,
305
- skipped: changes.filter((change) => change.action === "skip-existing").length,
306
- };
307
- });
308
- const changed = targets.some((target) => target.created > 0 || target.overwritten > 0);
309
- const onlySkipped = targets.every((target) => target.created === 0 && target.overwritten === 0 && target.skipped > 0);
310
- return {
311
- operation: "install-user",
312
- status: dryRun ? "dry-run" : changed ? "installed" : onlySkipped ? "already-present" : "no-op",
313
- dryRun,
314
- force,
315
- version: packageVersion(),
316
- source: repoRoot,
317
- targets,
318
- };
319
- }
320
-
321
- function readInstalledVersion(targetRoot) {
322
- try {
323
- const pkg = JSON.parse(fs.readFileSync(path.join(targetRoot, "package.json"), "utf8"));
324
- return pkg.version || "";
325
- } catch {
326
- return "";
327
- }
328
- }
329
-
330
- function commandOnPath(command) {
331
- const paths = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
332
- const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
333
- for (const base of paths) {
334
- for (const extension of extensions) {
335
- const candidate = path.join(base, `${command}${extension}`);
336
- if (fs.existsSync(candidate)) return candidate;
337
- }
338
- }
339
- return "";
340
- }
341
-
342
- export function doctorUserSkill({ agent = "codex", home = "" } = {}) {
343
- const required = [
344
- "SKILL.md",
345
- "package.json",
346
- "references",
347
- "templates",
348
- "templates-zh-CN",
349
- "scripts/harness.mjs",
350
- "docs-release/guides/agent-installation.md",
351
- ];
352
- const targets = normalizeUserAgent(agent).map((targetAgent) => {
353
- const target = targetForUserAgent(targetAgent, home);
354
- const missing = required.filter((relativePath) => !fs.existsSync(path.join(target, relativePath)));
355
- return {
356
- agent: targetAgent,
357
- target,
358
- status: missing.length === 0 ? "pass" : "fail",
359
- version: readInstalledVersion(target),
360
- missing,
361
- };
362
- });
363
- const harnessCommand = commandOnPath("harness");
364
- return {
365
- operation: "doctor-user",
366
- status: targets.every((target) => target.status === "pass") ? "pass" : "fail",
367
- version: packageVersion(),
368
- harnessCommand: harnessCommand || null,
369
- targets,
370
- };
371
- }
372
-
373
- export function validateCapabilities(target) {
374
- const registry = readCapabilityRegistry(target);
375
- const detected = detectCapabilities(target);
376
- const failures = [];
377
- const warnings = [];
378
- const byName = new Map(registry.capabilities.map((capability) => [capability.name, capability]));
379
-
380
- for (const error of registry.errors) failures.push(`invalid .harness-capabilities.json: ${error}`);
381
- for (const capability of registry.capabilities) {
382
- if (!capabilityDefinitions[capability.name]) {
383
- failures.push(`unknown capability: ${capability.name}`);
384
- continue;
385
- }
386
- if (!allowedCapabilityStates.has(capability.state)) {
387
- failures.push(`capability ${capability.name} has invalid state: ${capability.state}`);
388
- }
389
- for (const dependency of capabilityDefinitions[capability.name].dependencies) {
390
- if (!byName.has(dependency)) failures.push(`capability ${capability.name} missing dependency: ${dependency}`);
391
- }
392
- if (registry.mode === "declared-capability") {
393
- for (const artifact of capabilityDefinitions[capability.name].artifacts) {
394
- if (!exists(target, artifact)) {
395
- failures.push(`capability ${capability.name} missing required artifact: ${artifact}`);
396
- }
397
- }
398
- }
399
- }
400
-
401
- if (registry.mode === "declared-capability") {
402
- for (const capability of detected) {
403
- if (!byName.has(capability)) warnings.push(`orphan capability artifact detected without declaration: ${capability}`);
404
- }
405
- } else {
406
- warnings.push("legacy-compat mode: no .harness-capabilities.json; adoption suggestion is available");
407
- }
408
-
409
- return { registry, detected, failures, warnings };
410
- }
411
-
412
- function markdownTableRows(content) {
413
- return content
414
- .split(/\r?\n/)
415
- .filter((line) => line.trim().startsWith("|"))
416
- .map(splitMarkdownRow);
417
- }
418
-
419
- function slug(value) {
420
- return String(value || "item")
421
- .toLowerCase()
422
- .replace(/[^a-z0-9]+/g, "-")
423
- .replace(/^-+|-+$/g, "")
424
- .slice(0, 80) || "item";
425
- }
426
-
427
- function prefixedPath(target, filePath) {
428
- return `TARGET:${toPosix(path.relative(target.projectRoot, filePath))}`;
429
- }
430
-
431
- function sanitizeText(value) {
432
- return String(value ?? "")
433
- .replace(/file:\/\/\/[^\s)"'`<>\]]+/g, "LOCAL_FILE_URL_REDACTED")
434
- .replace(/\/Users\/[^/\s)"'`<>\]]+(?:\/[^\s)"'`<>\]]*)*/g, "LOCAL_PATH_REDACTED")
435
- .replace(/\/Volumes\/[^\s)"'`<>\]]+(?:\/[^\s)"'`<>\]]*)*/g, "LOCAL_PATH_REDACTED")
436
- .replace(/\/(?:private\/)?tmp\/[^\s)"'`<>\]]+(?:\/[^\s)"'`<>\]]*)*/g, "LOCAL_PATH_REDACTED")
437
- .replace(/\/var\/folders\/[^\s)"'`<>\]]+(?:\/[^\s)"'`<>\]]*)*/g, "LOCAL_PATH_REDACTED")
438
- .replace(/\/home\/[^/\s)"'`<>\]]+(?:\/[^\s)"'`<>\]]*)*/g, "LOCAL_PATH_REDACTED")
439
- .replace(/[A-Za-z]:\\[^\s)"'`<>\]]+(?:\\[^\s)"'`<>\]]*)*/g, "LOCAL_PATH_REDACTED");
440
- }
441
-
442
- function sanitizeDeep(value) {
443
- if (typeof value === "string") return sanitizeText(value);
444
- if (Array.isArray(value)) return value.map(sanitizeDeep);
445
- if (value && typeof value === "object") {
446
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeDeep(entry)]));
447
- }
448
- return value;
449
- }
450
-
451
- function titleFromMarkdown(content, fallback) {
452
- const match = content.match(/^#\s+(.+)$/m);
453
- return match ? match[1].trim() : fallback;
454
- }
455
-
456
- function parseAllMarkdownTables(content, source, kindPrefix) {
457
- const lines = content.split(/\r?\n/);
458
- const tables = [];
459
- let index = 0;
460
- let tableIndex = 1;
461
- while (index < lines.length) {
462
- if (!lines[index].trim().startsWith("|")) {
463
- index += 1;
464
- continue;
465
- }
466
- const start = index;
467
- const block = [];
468
- while (index < lines.length && lines[index].trim().startsWith("|")) {
469
- block.push(lines[index]);
470
- index += 1;
471
- }
472
- if (block.length < 2) continue;
473
- const rows = block.map(splitMarkdownRow);
474
- const separator = rows[1] || [];
475
- if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
476
- const columns = rows[0];
477
- const dataRows = rows.slice(2).filter((row) => row.length === columns.length);
478
- tables.push({
479
- id: `${slug(kindPrefix)}-${String(tableIndex).padStart(2, "0")}`,
480
- kind: kindPrefix,
481
- source,
482
- line: start + 1,
483
- columns,
484
- rows: dataRows.map((row, rowIndex) => ({
485
- id: `${slug(kindPrefix)}-${String(tableIndex).padStart(2, "0")}-row-${String(rowIndex + 1).padStart(3, "0")}`,
486
- cells: Object.fromEntries(columns.map((column, columnIndex) => [column, sanitizeText(row[columnIndex] || "")])),
487
- })),
488
- });
489
- tableIndex += 1;
490
- }
491
- return tables;
492
- }
493
-
494
- function splitMarkdownRow(line) {
495
- let text = String(line || "").trim();
496
- if (text.startsWith("|")) text = text.slice(1);
497
- if (text.endsWith("|") && !text.endsWith("\\|")) text = text.slice(0, -1);
498
- const cells = [];
499
- let current = "";
500
- let inCode = false;
501
- for (let index = 0; index < text.length; index += 1) {
502
- const char = text[index];
503
- if (char === "\\" && text[index + 1] === "|") {
504
- current += "|";
505
- index += 1;
506
- continue;
507
- }
508
- if (char === "`") inCode = !inCode;
509
- if (char === "|" && !inCode) {
510
- cells.push(current.trim());
511
- current = "";
512
- continue;
513
- }
514
- current += char;
515
- }
516
- cells.push(current.trim());
517
- return cells;
518
- }
519
-
520
- function tableAfterHeading(content, headerPattern) {
521
- const rows = markdownTableRows(content);
522
- const headerIndex = rows.findIndex((cells) => cells.some((cell) => headerPattern.test(cell)));
523
- if (headerIndex < 0) return { header: [], rows: [] };
524
- const header = rows[headerIndex];
525
- const body = [];
526
- for (const row of rows.slice(headerIndex + 1)) {
527
- if (row.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
528
- if (row.length !== header.length) break;
529
- body.push(row);
530
- }
531
- return { header, rows: body };
532
- }
533
-
534
- function getColumn(header, name) {
535
- return header.findIndex((cell) => cell.toLowerCase() === name.toLowerCase());
536
- }
537
-
538
- function parseTaskState(progressContent) {
539
- const match = progressContent.match(/^##\s*(?:Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
540
- const raw = match ? match[1].replace(/`/g, "").trim() : "unknown";
541
- const aliases = new Map([
542
- ["进行中", "in_progress"],
543
- ["已完成", "completed"],
544
- ["未开始", "not_started"],
545
- ["已阻塞", "blocked"],
546
- ]);
547
- return aliases.get(raw) || raw.toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
548
- }
549
-
550
- function parsePhases(taskPlanContent) {
551
- const { header, rows } = tableAfterHeading(taskPlanContent, /^Phase ID$/i);
552
- if (rows.length === 0) return [];
553
- const indexes = {
554
- id: getColumn(header, "Phase ID"),
555
- dependsOn: getColumn(header, "Depends On"),
556
- state: getColumn(header, "State"),
557
- completion: getColumn(header, "Completion"),
558
- output: getColumn(header, "Output"),
559
- requiredEvidence: getColumn(header, "Required Evidence"),
560
- evidenceStatus: getColumn(header, "Evidence Status"),
561
- blockingRisk: getColumn(header, "Blocking Risk"),
562
- owner: getColumn(header, "Owner / Handoff"),
563
- };
564
- return rows.map((row) => ({
565
- id: row[indexes.id] || "",
566
- dependsOn: splitDependencies(row[indexes.dependsOn] || ""),
567
- state: row[indexes.state] || "planned",
568
- completion: Number.parseInt(String(row[indexes.completion] || "0").replace("%", ""), 10) || 0,
569
- output: row[indexes.output] || "",
570
- requiredEvidence: splitList(row[indexes.requiredEvidence] || ""),
571
- evidenceStatus: row[indexes.evidenceStatus] || "missing",
572
- blockingRisk: row[indexes.blockingRisk] || "",
573
- owner: row[indexes.owner] || "",
574
- }));
575
- }
576
-
577
- function readTaskContractFile(taskDir, fileName, legacyContent = "") {
578
- const filePath = path.join(taskDir, fileName);
579
- const content = readFileSafe(filePath);
580
- if (content.trim()) return { path: filePath, content, source: "standalone" };
581
- return { path: filePath, content: legacyContent, source: legacyContent.trim() ? "legacy" : "missing" };
582
- }
583
-
584
- function splitList(value) {
585
- return String(value || "")
586
- .split(/[,+;]/)
587
- .map((item) => item.trim())
588
- .filter(Boolean)
589
- .filter((item) => item.toLowerCase() !== "none");
590
- }
591
-
592
- function splitDependencies(value) {
593
- return String(value || "")
594
- .split(/\s*(?:,|;|\+|&|\/|\band\b|\bAND\b)\s*/)
595
- .map((item) => item.trim())
596
- .filter(Boolean)
597
- .filter((item) => !/^(none|n\/a|na|-|—|–|无)$/i.test(item))
598
- .filter((item) => !/^same\b/i.test(item));
599
- }
600
-
601
- function listTaskPlanPaths(target) {
602
- const taskRoots = [
603
- path.join(target.docsRoot, "09-PLANNING/TASKS"),
604
- path.join(target.docsRoot, "09-PLANNING/MODULES"),
605
- ];
606
- return taskRoots
607
- .flatMap(walkFiles)
608
- .filter((file) => file.endsWith("task_plan.md"))
609
- .filter((file) => !file.includes(`${path.sep}_task-template${path.sep}`))
610
- .filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
611
- .filter((file) => !file.includes(`${path.sep}_archive${path.sep}`));
612
- }
613
-
614
- export function collectTasks(target) {
615
- return listTaskPlanPaths(target).map((taskPlanPath) => {
616
- const taskDir = path.dirname(taskPlanPath);
617
- const taskPlan = readFileSafe(taskPlanPath);
618
- const roadmap = readTaskContractFile(taskDir, "visual_roadmap.md", taskPlan);
619
- const progress = readFileSafe(path.join(taskDir, "progress.md"));
620
- const review = readFileSafe(path.join(taskDir, "review.md"));
621
- const phases = parsePhases(roadmap.content);
622
- const completion =
623
- phases.length > 0
624
- ? Math.round(
625
- phases.filter((phase) => phase.state !== "skipped").reduce((sum, phase) => sum + phase.completion, 0) /
626
- Math.max(1, phases.filter((phase) => phase.state !== "skipped").length),
627
- )
628
- : 0;
629
- const relative = toPosix(path.relative(target.projectRoot, taskDir));
630
- const title = path.basename(taskDir);
631
- return {
632
- id: title,
633
- title,
634
- path: `TARGET:${relative}`,
635
- roadmapSource: roadmap.source,
636
- state: parseTaskState(progress),
637
- completion,
638
- phases,
639
- risks: collectReviewRisks(review),
640
- evidence: collectEvidence(progress),
641
- handoffs: collectHandoffs(progress, title),
642
- dependencies: [],
643
- };
644
- });
645
- }
646
-
647
- function collectMarkdownDocuments(target) {
648
- const docs = collectDashboardDocumentPaths(target);
649
- return docs.map((file, index) => {
650
- const content = sanitizeText(readFileSafe(file));
651
- const source = prefixedPath(target, file);
652
- return {
653
- id: `doc-${String(index + 1).padStart(4, "0")}-${slug(path.basename(file, ".md"))}`,
654
- path: source,
655
- title: titleFromMarkdown(content, path.basename(file)),
656
- content,
657
- };
658
- });
659
- }
660
-
661
- function collectDashboardDocumentPaths(target) {
662
- const selected = new Set();
663
- const addDocsPath = (relativePath) => {
664
- const file = path.join(target.docsRoot, relativePath);
665
- if (fs.existsSync(file)) selected.add(file);
666
- };
667
- for (const relativePath of [
668
- "Harness-Ledger.md",
669
- "09-PLANNING/Module-Registry.md",
670
- "05-TEST-QA/Regression-SSoT.md",
671
- "05-TEST-QA/Cadence-Ledger.md",
672
- "01-GOVERNANCE/Lessons-SSoT.md",
673
- "10-WALKTHROUGH/Closeout-SSoT.md",
674
- ]) {
675
- addDocsPath(relativePath);
676
- }
677
- for (const taskPlanPath of listTaskPlanPaths(target)) {
678
- const taskDir = path.dirname(taskPlanPath);
679
- for (const fileName of ["task_plan.md", "execution_strategy.md", "visual_roadmap.md", "progress.md", "review.md", "findings.md"]) {
680
- const file = path.join(taskDir, fileName);
681
- if (fs.existsSync(file)) selected.add(file);
682
- }
683
- for (const indexFile of ["references/INDEX.md", "artifacts/INDEX.md"]) {
684
- const file = path.join(taskDir, indexFile);
685
- if (fs.existsSync(file)) selected.add(file);
686
- }
687
- }
688
- for (const file of walkFiles(path.join(target.docsRoot, "09-PLANNING/MODULES"))) {
689
- if (file.endsWith("module_plan.md")) selected.add(file);
690
- }
691
- for (const file of walkFiles(path.join(target.docsRoot, "01-GOVERNANCE/lessons"))) {
692
- if (file.endsWith(".md")) selected.add(file);
693
- }
694
- return [...selected]
695
- .filter((file) => !file.includes(`${path.sep}_archive${path.sep}`))
696
- .filter((file) => !file.includes(`${path.sep}_task-template${path.sep}`))
697
- .filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
698
- .sort();
699
- }
700
-
701
- function documentKind(source) {
702
- const lower = source.toLowerCase();
703
- if (lower.includes("harness-ledger.md")) return "harness-ledger";
704
- if (lower.includes("module-registry.md")) return "module-registry";
705
- if (lower.includes("regression-ssot.md")) return "regression-ssot";
706
- if (lower.includes("cadence-ledger.md")) return "cadence-ledger";
707
- if (lower.includes("lessons-ssot.md")) return "lessons-ssot";
708
- if (lower.endsWith("/progress.md")) return "task-progress";
709
- if (lower.endsWith("/review.md")) return "task-review";
710
- if (lower.endsWith("/references/index.md")) return "task-references";
711
- if (lower.endsWith("/artifacts/index.md")) return "task-artifacts";
712
- if (lower.endsWith("/execution_strategy.md")) return "execution-strategy";
713
- if (lower.endsWith("/visual_roadmap.md")) return "visual-roadmap";
714
- if (lower.endsWith("/module_plan.md")) return "module-plan";
715
- return "markdown-table";
716
- }
717
-
718
- function collectTables(documents) {
719
- return {
720
- tables: documents.flatMap((document) => parseAllMarkdownTables(document.content, document.path, documentKind(document.path))),
721
- };
722
- }
723
-
724
- function collectGraph(status, tables = { tables: [] }) {
725
- const nodes = [];
726
- const edges = [];
727
- const seenNodes = new Set();
728
- const addNode = (node) => {
729
- if (seenNodes.has(node.id)) return;
730
- seenNodes.add(node.id);
731
- nodes.push(node);
732
- };
733
- const addEdge = (edge) => {
734
- if (!edge.from || !edge.to || edge.from === edge.to) return;
735
- edges.push(edge);
736
- };
737
- for (const task of status.tasks) {
738
- addNode({ id: `task:${task.id}`, type: "task", label: task.title, state: task.state, completion: task.completion });
739
- for (const phase of task.phases || []) {
740
- const phaseId = `phase:${task.id}:${phase.id}`;
741
- addNode({ id: phaseId, type: "phase", label: phase.id, state: phase.state, completion: phase.completion, taskId: task.id });
742
- addEdge({ from: `task:${task.id}`, to: phaseId, type: "contains" });
743
- for (const dependency of phase.dependsOn || []) {
744
- addEdge({ from: `phase:${task.id}:${dependency}`, to: phaseId, type: "depends_on" });
745
- }
746
- }
747
- for (const handoff of task.handoffs || []) {
748
- const handoffId = `handoff:${handoff.id}`;
749
- addNode({ id: handoffId, type: "handoff", label: handoff.summary, state: handoff.state });
750
- addEdge({ from: `task:${task.id}`, to: handoffId, type: "handoff" });
751
- }
752
- }
753
- for (const table of tables.tables || []) {
754
- if (table.kind === "module-registry") {
755
- for (const row of table.rows) {
756
- const key = row.cells.Key || row.cells.Module || "";
757
- if (!key) continue;
758
- const moduleId = `module:${key}`;
759
- addNode({ id: moduleId, type: "module", label: row.cells.Name || key, state: row.cells.Status || "unknown", currentStep: row.cells["Current Step"] || "" });
760
- if (row.cells["Current Step"]) {
761
- const stepId = `step:${row.cells["Current Step"]}`;
762
- addNode({ id: stepId, type: "step", label: row.cells["Current Step"], state: row.cells.Status || "unknown", module: key });
763
- addEdge({ from: moduleId, to: stepId, type: "current_step" });
764
- }
765
- }
766
- }
767
- if (table.kind === "module-plan") {
768
- const moduleMatch = table.source.match(/MODULES\/([^/]+)\/module_plan\.md$/);
769
- const moduleKey = moduleMatch ? moduleMatch[1] : slug(table.source);
770
- const moduleId = `module:${moduleKey}`;
771
- addNode({ id: moduleId, type: "module", label: moduleKey, state: "planned" });
772
- for (const row of table.rows) {
773
- const step = row.cells["Step ID"];
774
- if (!step) continue;
775
- const stepId = `step:${step}`;
776
- addNode({ id: stepId, type: "step", label: `${step} ${row.cells.Name || ""}`.trim(), state: row.cells.Status || "unknown", module: moduleKey });
777
- addEdge({ from: moduleId, to: stepId, type: "contains" });
778
- for (const dependency of splitDependencies(row.cells["Depends On"] || "")) {
779
- addEdge({ from: `step:${dependency}`, to: stepId, type: "depends_on" });
780
- }
781
- }
782
- }
783
- }
784
- for (const edge of edges) {
785
- if (edge.type === "depends_on" && !seenNodes.has(edge.from)) {
786
- addNode({ id: edge.from, type: "external-dependency", label: edge.from.replace(/^(phase:[^:]+:|step:)/, ""), state: "external" });
787
- }
788
- }
789
- return { nodes, edges: edges.filter((edge) => seenNodes.has(edge.from) && seenNodes.has(edge.to)) };
790
- }
791
-
792
- function categorizeWarning(message) {
793
- if (/missing execution_strategy\.md|missing visual_roadmap\.md|Visual Roadmap/i.test(message)) return "Plan Contract Missing";
794
- if (/legacy-compat|adoption-needed|legacy check/i.test(message)) return "Adoption Advice";
795
- if (/Evidence|evidence/i.test(message)) return "Missing Evidence";
796
- if (/schema|missing .*columns|invalid/i.test(message)) return "Schema Drift";
797
- return "Review Finding";
798
- }
799
-
800
- function collectAdoption(status) {
801
- const warnings = status.checkState.details.warnings.flatMap((message) => splitWarningMessage(message)).map((message, index) => ({
802
- id: `AD-${String(index + 1).padStart(3, "0")}`,
803
- category: categorizeWarning(message),
804
- severity: status.mode === "legacy-compat" ? "advice" : "warning",
805
- title: warningTitle(message),
806
- affected: warningAffected(message),
807
- requiredAction: warningAction(message),
808
- detail: sanitizeText(message),
809
- }));
810
- return {
811
- mode: status.mode,
812
- project: status.project,
813
- summary: {
814
- blockers: status.checkState.failures,
815
- advice: warnings.length,
816
- },
817
- warnings,
818
- manualSteps: {
819
- zh: [
820
- "先查看升级建议,决定当前项目要采用哪些 v1.0 能力合同。",
821
- "为仍在活跃的任务手工补齐 execution_strategy.md 和 visual_roadmap.md。",
822
- "只有在项目明确声明 v1.0 capability 后,再把 strict check 当成阻塞门禁。",
823
- ],
824
- en: [
825
- "Review adoption advice and decide which v1.0 capability contracts should be adopted.",
826
- "Manually add execution_strategy.md and visual_roadmap.md for active tasks.",
827
- "Treat strict check as blocking only after the project intentionally declares v1.0 capabilities.",
828
- ],
829
- },
830
- };
831
- }
832
-
833
- function splitWarningMessage(message) {
834
- return String(message || "")
835
- .split(/\n-\s+/)
836
- .map((item, index) => (index === 0 ? item : `- ${item}`))
837
- .filter(Boolean);
838
- }
839
-
840
- function warningTitle(message) {
841
- if (/missing execution_strategy\.md/i.test(message)) return "Missing execution strategy";
842
- if (/missing visual_roadmap\.md|Visual Roadmap/i.test(message)) return "Missing visual roadmap";
843
- if (/legacy-compat/i.test(message)) return "Legacy compatibility mode";
844
- if (/legacy check failed/i.test(message)) return "Legacy checker finding";
845
- if (/review\.md missing/i.test(message)) return "Review schema gap";
846
- if (/findings table missing/i.test(message)) return "Review findings schema gap";
847
- return String(message).split(":")[0].slice(0, 96);
848
- }
849
-
850
- function warningAffected(message) {
851
- const target = String(message).match(/(?:docs|\.harness-private)\/[^\s:]+/);
852
- return target ? target[0] : "project";
853
- }
854
-
855
- function warningAction(message) {
856
- if (/execution_strategy\.md/i.test(message)) return "Add standalone execution strategy file.";
857
- if (/visual_roadmap\.md|Visual Roadmap/i.test(message)) return "Add standalone visual roadmap phase table.";
858
- if (/review\.md missing/i.test(message)) return "Update review.md to v1 review schema.";
859
- if (/legacy/i.test(message)) return "Review manually; do not auto-migrate.";
860
- return "Inspect source document and decide whether to adopt v1 contract.";
861
- }
862
-
863
- export function buildDashboardBundle(targetInput, options = {}) {
864
- const status = buildStatus(targetInput, options);
865
- const target = normalizeTarget(targetInput);
866
- const documents = { documents: collectMarkdownDocuments(target) };
867
- const tables = collectTables(documents.documents);
868
- const graph = collectGraph(status, tables);
869
- const adoption = collectAdoption(status);
870
- return sanitizeDeep({ status, tables, documents, graph, adoption });
871
- }
872
-
873
- export function writeDashboardFolder(outDir, targetInput, options = {}) {
874
- const target = normalizeTarget(targetInput);
875
- const registry = readCapabilityRegistry(target);
876
- const bundle = buildDashboardBundle(targetInput, options);
877
- return writeDashboardDirectory(outDir, bundle, { repoRoot, projectRoot: target.projectRoot, docsRoot: target.docsRoot, locale: registry.locale });
878
- }
879
-
880
- function collectHandoffs(progressContent, taskId) {
881
- if (!/Coordinator Handoff/i.test(progressContent) || !/pending-coordinator-pass/i.test(progressContent)) return [];
882
- return [{ id: `H-${taskId}`, from: "worker", to: "coordinator", state: "pending", summary: "Coordinator handoff pending" }];
883
- }
884
-
885
- function collectReviewRisks(reviewContent) {
886
- const { header, rows } = tableAfterHeading(reviewContent, /^ID$/i);
887
- const severityIndex = getColumn(header, "Severity");
888
- const findingIndex = getColumn(header, "Finding");
889
- const openIndex = getColumn(header, "Open");
890
- const blocksIndex = getColumn(header, "Blocks Release");
891
- if (severityIndex < 0 || findingIndex < 0) return [];
892
- return rows
893
- .filter((row) => /^P[0-3]$/i.test(row[severityIndex] || ""))
894
- .map((row) => ({
895
- id: row[0],
896
- severity: row[severityIndex],
897
- open: /^yes$/i.test(row[openIndex] || "no"),
898
- blocksRelease: /^yes$/i.test(row[blocksIndex] || "no"),
899
- summary: row[findingIndex],
900
- }));
901
- }
902
-
903
- function collectEvidence(progressContent) {
904
- const matches = [...progressContent.matchAll(/\b(command|diff|fixture|screenshot|review|report):((?:PUBLIC|PRIVATE|TARGET|EXTERNAL|URL):[^:\s|]+):([^\n|]+)/g)];
905
- return matches.map((match, index) => ({
906
- id: `E-${String(index + 1).padStart(3, "0")}`,
907
- type: match[1],
908
- path: match[2],
909
- status: "present",
910
- summary: match[3].trim(),
911
- }));
912
- }
913
-
914
- export function runLegacyCheck(target) {
915
- const checkTarget = target.docsOnly ? target.projectRoot : target.input;
916
- const result = spawnSync(process.execPath, [legacyChecker, checkTarget], {
917
- cwd: repoRoot,
918
- encoding: "utf8",
919
- });
920
- return {
921
- status: result.status === 0 ? "pass" : "fail",
922
- code: result.status ?? 1,
923
- stdout: result.stdout || "",
924
- stderr: result.stderr || "",
925
- };
926
- }
927
-
928
- export function validateReviewSchema(target, { strict = true } = {}) {
929
- const failures = [];
930
- const warnings = [];
931
- const report = (message) => {
932
- if (strict) failures.push(message);
933
- else warnings.push(`adoption-needed: ${message}`);
934
- };
935
- const reviewPaths = walkFiles(target.docsRoot)
936
- .filter((file) => file.endsWith("review.md"))
937
- .filter((file) => !file.includes(`${path.sep}_task-template${path.sep}`))
938
- .filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
939
- .filter((file) => !file.includes(`${path.sep}_archive${path.sep}`));
940
-
941
- for (const reviewPath of reviewPaths) {
942
- const relative = toPosix(path.relative(target.projectRoot, reviewPath));
943
- const content = readFileSafe(reviewPath);
944
- for (const required of ["Reviewer Identity", "Confidence Challenge", "Evidence Checked", "Final Confidence Basis"]) {
945
- if (!content.includes(required)) {
946
- if (strict) failures.push(`${relative} missing ${required}`);
947
- else warnings.push(`${relative} missing ${required}`);
948
- }
949
- }
950
- const evidenceTable = tableAfterHeading(content, /^Evidence ID$/i);
951
- if (strict && evidenceTable.rows.length === 0) {
952
- failures.push(`${relative} Evidence Checked table needs at least one evidence row`);
953
- }
954
- const usesVerifier = /verifier-backed|(^|\|)[^|\n]*\|\s*verifier\s*\|/im.test(content);
955
- if (usesVerifier) {
956
- if (!/template_id:\s*`?harness-verifier\/v1`?/i.test(content)) {
957
- report(`${relative} verifier-backed review missing template_id: harness-verifier/v1`);
958
- }
959
- if (!/verdict:\s*`?(pass|fail|inconclusive)`?/i.test(content)) {
960
- report(`${relative} verifier-backed review missing verdict`);
961
- }
962
- }
963
- const { header, rows } = tableAfterHeading(content, /^ID$/i);
964
- if (rows.length === 0) continue;
965
- const severityIndex = getColumn(header, "Severity");
966
- const openIndex = getColumn(header, "Open");
967
- const dispositionIndex = getColumn(header, "Disposition");
968
- const blocksIndex = getColumn(header, "Blocks Release");
969
- const followUpIndex = getColumn(header, "Follow-up");
970
- const evidenceCheckedIndex = getColumn(header, "Evidence Checked");
971
- if ([severityIndex, openIndex, dispositionIndex, blocksIndex].some((index) => index < 0)) {
972
- report(`${relative} findings table missing Severity/Open/Disposition/Blocks Release columns`);
973
- continue;
974
- }
975
- for (const row of rows) {
976
- const id = row[0] || "";
977
- if (!/^(R|SR)-\d+/i.test(id)) continue;
978
- const severity = row[severityIndex] || "";
979
- const open = (row[openIndex] || "").toLowerCase();
980
- const disposition = (row[dispositionIndex] || "").toLowerCase();
981
- const blocks = (row[blocksIndex] || "").toLowerCase();
982
- const followUp = row[followUpIndex] || "";
983
- if (!/^P[0-3]$/.test(severity)) report(`${relative} ${id} invalid severity: ${severity}`);
984
- if (!["yes", "no"].includes(open)) report(`${relative} ${id} invalid Open value: ${open}`);
985
- if (!allowedReviewDispositions.has(disposition)) report(`${relative} ${id} invalid Disposition: ${disposition}`);
986
- if (!["yes", "no"].includes(blocks)) report(`${relative} ${id} invalid Blocks Release value: ${blocks}`);
987
- if ((open === "yes" || blocks === "yes") && /^P[01]$/.test(severity)) {
988
- report(`${relative} ${id} has release-blocking open ${severity}`);
989
- }
990
- if (["accepted-risk", "deferred"].includes(disposition) && (!followUp || /^none|无$/i.test(followUp))) {
991
- report(`${relative} ${id} ${disposition} requires follow-up routing`);
992
- }
993
- if (strict && evidenceCheckedIndex >= 0) {
994
- const refs = splitList(row[evidenceCheckedIndex] || "");
995
- const evidenceIds = new Set(evidenceTable.rows.map((evidenceRow) => evidenceRow[0]));
996
- for (const ref of refs) {
997
- if (ref !== "none" && /^E-\d+/i.test(ref) && !evidenceIds.has(ref)) {
998
- failures.push(`${relative} ${id} references missing evidence id: ${ref}`);
999
- }
1000
- }
1001
- }
1002
- }
1003
- }
1004
- return { failures, warnings };
1005
- }
1006
-
1007
- export function validateVisualRoadmaps(target) {
1008
- const failures = [];
1009
- const warnings = [];
1010
- for (const taskPlanPath of listTaskPlanPaths(target)) {
1011
- const taskDir = path.dirname(taskPlanPath);
1012
- const roadmapPath = path.join(taskDir, "visual_roadmap.md");
1013
- const relative = toPosix(path.relative(target.projectRoot, roadmapPath));
1014
- const taskPlan = readFileSafe(taskPlanPath);
1015
- const roadmap = readTaskContractFile(taskDir, "visual_roadmap.md", taskPlan);
1016
- const { header, rows } = tableAfterHeading(roadmap.content, /^Phase ID$/i);
1017
- if (rows.length > 0) {
1018
- for (const column of ["Phase ID", "Depends On", "State", "Completion", "Output", "Required Evidence", "Evidence Status", "Blocking Risk", "Owner / Handoff"]) {
1019
- if (getColumn(header, column) < 0) failures.push(`${relative} Visual Roadmap missing column: ${column}`);
1020
- }
1021
- }
1022
- const phases = parsePhases(roadmap.content);
1023
- for (const phase of phases) {
1024
- if (!allowedPhaseStates.has(phase.state)) failures.push(`${relative} phase ${phase.id} invalid state: ${phase.state}`);
1025
- if (!allowedEvidenceStatus.has(phase.evidenceStatus)) {
1026
- failures.push(`${relative} phase ${phase.id} invalid evidence status: ${phase.evidenceStatus}`);
1027
- }
1028
- if (!Number.isInteger(phase.completion) || phase.completion < 0 || phase.completion > 100) {
1029
- failures.push(`${relative} phase ${phase.id} completion must be integer 0..100`);
1030
- }
1031
- if (phase.state === "done" && phase.completion !== 100) failures.push(`${relative} phase ${phase.id} done must be 100`);
1032
- if (phase.state === "planned" && phase.completion !== 0) failures.push(`${relative} phase ${phase.id} planned must be 0`);
1033
- }
1034
- if (roadmap.source === "standalone" && phases.length === 0) warnings.push(`${relative} has no Visual Roadmap phase table`);
1035
- if (roadmap.source === "legacy" && phases.length > 0) warnings.push(`${relative} missing; using legacy task_plan.md Visual Roadmap fallback`);
1036
- }
1037
- return { failures, warnings };
1038
- }
1039
-
1040
- export function validatePlanContracts(target, { strict = true } = {}) {
1041
- const failures = [];
1042
- const warnings = [];
1043
- const report = (message) => {
1044
- if (strict) failures.push(message);
1045
- else warnings.push(`adoption-needed: ${message}`);
1046
- };
1047
- for (const taskPlanPath of listTaskPlanPaths(target)) {
1048
- const taskDir = path.dirname(taskPlanPath);
1049
- const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
1050
- for (const fileName of ["execution_strategy.md", "visual_roadmap.md"]) {
1051
- if (!fs.existsSync(path.join(taskDir, fileName))) {
1052
- report(`${relativeDir} missing ${fileName}`);
1053
- }
1054
- }
1055
- }
1056
- return { failures, warnings };
1057
- }
1058
-
1059
- export function buildStatus(targetInput, options = {}) {
1060
- const target = normalizeTarget(targetInput);
1061
- const capabilityState = validateCapabilities(target);
1062
- const declaredCapabilities = new Set(capabilityState.registry.capabilities.map((capability) => capability.name));
1063
- const safeAdoptionMode = declaredCapabilities.has("safe-adoption");
1064
- const shouldRunLegacy = !options.skipLegacyCheck && (capabilityState.registry.mode === "legacy-compat" || safeAdoptionMode);
1065
- const legacy = shouldRunLegacy ? runLegacyCheck(target) : { status: "skipped", code: 0, stdout: "", stderr: "" };
1066
- const contractStrict = Boolean(options.strict) || (capabilityState.registry.mode !== "legacy-compat" && !safeAdoptionMode);
1067
- const reviews = validateReviewSchema(target, { strict: contractStrict });
1068
- const roadmaps = validateVisualRoadmaps(target);
1069
- const planContracts = validatePlanContracts(target, { strict: contractStrict });
1070
- const failures = [...capabilityState.failures, ...reviews.failures, ...roadmaps.failures, ...planContracts.failures];
1071
- const warnings = [...capabilityState.warnings, ...reviews.warnings, ...roadmaps.warnings, ...planContracts.warnings];
1072
- if (legacy.status === "fail") {
1073
- if (options.strictLegacy) failures.push("legacy check failed");
1074
- else warnings.push(`adoption-needed: legacy check failed: ${(legacy.stderr || legacy.stdout).trim()}`);
1075
- }
1076
-
1077
- const tasks = collectTasks(target);
1078
- const capabilityNames = new Map(capabilityState.registry.capabilities.map((capability) => [capability.name, capability]));
1079
- for (const detected of capabilityState.detected) {
1080
- if (!capabilityNames.has(detected)) capabilityNames.set(detected, { name: detected, state: "configured" });
1081
- }
1082
-
1083
- return {
1084
- project: {
1085
- name: path.basename(target.projectRoot),
1086
- root: `TARGET:${target.docsOnly ? toPosix(path.relative(target.projectRoot, target.docsRoot)) : "."}`,
1087
- docsOnly: target.docsOnly,
1088
- },
1089
- generatedAt: new Date().toISOString(),
1090
- mode: capabilityState.registry.mode,
1091
- checkState: {
1092
- status: failures.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass",
1093
- failures: failures.length,
1094
- warnings: warnings.length,
1095
- details: { failures, warnings },
1096
- legacy,
1097
- },
1098
- capabilities: [...capabilityNames.values()].map((capability) => ({
1099
- name: capability.name,
1100
- state: capability.state || "configured",
1101
- dependencyStatus: capabilityDefinitions[capability.name]?.dependencies.every((dependency) => capabilityNames.has(dependency))
1102
- ? "valid"
1103
- : "invalid",
1104
- warnings: capabilityState.warnings.filter((warning) => warning.includes(capability.name)),
1105
- })),
1106
- tasks,
1107
- handoffs: tasks.flatMap((task) => task.handoffs || []),
1108
- recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
1109
- };
1110
- }
1111
-
1112
- export function renderDashboard(status) {
1113
- const taskCards = status.tasks
1114
- .map((task) => {
1115
- const phases = task.phases
1116
- .map(
1117
- (phase) => `<div class="phase ${escapeHtml(phase.state)}">
1118
- <div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${phase.completion}%</span></div>
1119
- <div class="phase-output">${escapeHtml(phase.output)}</div>
1120
- <div class="meter"><i style="width:${phase.completion}%"></i></div>
1121
- <div class="muted">${escapeHtml(phase.state)} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
1122
- </div>`,
1123
- )
1124
- .join("");
1125
- const risks = task.risks
1126
- .map((risk) => `<span class="risk ${risk.open || risk.blocksRelease ? "open" : ""}">${escapeHtml(risk.severity)} ${escapeHtml(risk.summary)}</span>`)
1127
- .join("");
1128
- const evidence = task.evidence
1129
- .map((item) => `<span class="evidence">${escapeHtml(item.type)} · ${escapeHtml(item.summary)}</span>`)
1130
- .join("");
1131
- const evidenceMeter = evidenceCompletion(task.phases);
1132
- return `<section class="task">
1133
- <div class="task-head">
1134
- <div><h2>${escapeHtml(task.title)}</h2><p>${escapeHtml(task.path)}</p></div>
1135
- <div class="score">${task.completion}%</div>
1136
- </div>
1137
- <div class="meter"><i style="width:${task.completion}%"></i></div>
1138
- <div class="phases">${phases || '<div class="empty">No phase table</div>'}</div>
1139
- <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>
1140
- <div class="risks">${risks || '<span class="ok">No open visual risk</span>'}</div>
1141
- </section>`;
1142
- })
1143
- .join("");
1144
- const chips = status.capabilities
1145
- .map((capability) => `<span class="chip ${escapeHtml(capability.state)}">${escapeHtml(capability.name)} · ${escapeHtml(capability.state)}</span>`)
1146
- .join("");
1147
- const failures = status.checkState.details.failures.map((failure) => `<li>${escapeHtml(failure)}</li>`).join("");
1148
- const warnings = status.checkState.details.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("");
1149
- const handoffs = status.handoffs
1150
- .map((handoff) => `<span class="handoff">${escapeHtml(handoff.state)} · ${escapeHtml(handoff.summary)}</span>`)
1151
- .join("");
1152
- const activity = status.recentActivity
1153
- .map((item) => `<li><strong>${escapeHtml(item.type)}</strong> ${escapeHtml(item.summary)}</li>`)
1154
- .join("");
1155
- return `<!doctype html>
1156
- <html lang="zh-CN">
1157
- <head>
1158
- <meta charset="utf-8">
1159
- <meta name="viewport" content="width=device-width, initial-scale=1">
1160
- <title>${escapeHtml(status.project.name)} Harness Dashboard</title>
1161
- <style>
1162
- :root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#17202a;background:#f6f7f9}
1163
- body{margin:0}.shell{max-width:1180px;margin:0 auto;padding:28px}
1164
- header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}
1165
- h1,h2{margin:0;letter-spacing:0}h1{font-size:30px}h2{font-size:18px}p{margin:6px 0;color:#687382}
1166
- .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}
1167
- .pass,.verified{background:#dff5e8;color:#125c32}.warn,.configured{background:#fff0cc;color:#765100}.fail,.open{background:#ffe1df;color:#8a1c12}.scaffolded{background:#e8edf3;color:#273444}
1168
- .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}
1169
- .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}
1170
- .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}
1171
- .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}
1172
- .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}
1173
- .risks{margin-top:12px}.empty{color:#8a95a3}.panel{background:#fff;border:1px solid #e4e8ee;border-radius:8px;padding:16px;margin-top:16px}
1174
- @media(max-width:760px){.shell{padding:16px}header{display:block}.grid{grid-template-columns:1fr 1fr}.task-head{display:block}}
1175
- </style>
1176
- </head>
1177
- <body><main class="shell">
1178
- <header>
1179
- <div><h1>${escapeHtml(status.project.name)} Harness Dashboard</h1><p>${escapeHtml(status.project.root)} · ${escapeHtml(status.generatedAt)}</p></div>
1180
- <span class="pill ${escapeHtml(status.checkState.status)}">${escapeHtml(status.checkState.status)} · ${escapeHtml(status.mode)}</span>
1181
- </header>
1182
- <section class="grid">
1183
- <div class="stat"><strong>${status.tasks.length}</strong><span>Tasks</span></div>
1184
- <div class="stat"><strong>${status.capabilities.length}</strong><span>Capabilities</span></div>
1185
- <div class="stat"><strong>${status.checkState.failures}</strong><span>Failures</span></div>
1186
- <div class="stat"><strong>${status.checkState.warnings}</strong><span>Warnings</span></div>
1187
- </section>
1188
- <section class="capabilities">${chips}</section>
1189
- <section class="panel"><h2>Handoffs</h2>${handoffs || '<span class="ok">No pending handoff</span>'}</section>
1190
- ${taskCards || '<section class="task">No tasks found.</section>'}
1191
- <section class="panel"><h2>Recent Activity</h2><ul>${activity || "<li>None</li>"}</ul></section>
1192
- <section class="panel"><h2>Failures</h2><ul>${failures || "<li>None</li>"}</ul><h2>Warnings</h2><ul>${warnings || "<li>None</li>"}</ul></section>
1193
- </main></body></html>`;
1194
- }
1195
-
1196
- function escapeHtml(value) {
1197
- return String(value ?? "")
1198
- .replaceAll("&", "&amp;")
1199
- .replaceAll("<", "&lt;")
1200
- .replaceAll(">", "&gt;")
1201
- .replaceAll('"', "&quot;");
1202
- }
1203
-
1204
- function evidenceCompletion(phases) {
1205
- const scored = phases.filter((phase) => phase.state !== "skipped");
1206
- if (scored.length === 0) return 0;
1207
- const score = scored.reduce((sum, phase) => {
1208
- if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
1209
- if (phase.evidenceStatus === "partial") return sum + 50;
1210
- return sum;
1211
- }, 0);
1212
- return Math.round(score / scored.length);
1213
- }
1214
-
1215
- function localizedTemplateSource(source, locale) {
1216
- const localeSource = normalizeLocale(locale) === "zh-CN" ? source.replace(/^templates\//, "templates-zh-CN/") : source;
1217
- return fs.existsSync(path.join(repoRoot, localeSource)) ? localeSource : source;
1218
- }
1219
-
1220
- export function plannedInitFiles(capabilities = ["core"], { locale = "en-US" } = {}) {
1221
- const files = [
1222
- ["AGENTS.md", "templates/AGENTS.md.template"],
1223
- ["CLAUDE.md", "templates/CLAUDE.md.template"],
1224
- ["docs/Harness-Ledger.md", "templates/ledger/Harness-Ledger.md"],
1225
- ["docs/09-PLANNING/TASKS/_task-template/task_plan.md", "templates/planning/task_plan.md"],
1226
- ["docs/09-PLANNING/TASKS/_task-template/execution_strategy.md", "templates/planning/execution_strategy.md"],
1227
- ["docs/09-PLANNING/TASKS/_task-template/visual_roadmap.md", "templates/planning/visual_roadmap.md"],
1228
- ["docs/09-PLANNING/TASKS/_task-template/findings.md", "templates/planning/findings.md"],
1229
- ["docs/09-PLANNING/TASKS/_task-template/progress.md", "templates/planning/progress.md"],
1230
- ["docs/09-PLANNING/TASKS/_task-template/review.md", "templates/planning/review.md"],
1231
- ["docs/05-TEST-QA/Regression-SSoT.md", "templates/ssot/Regression-SSoT.md"],
1232
- ["docs/05-TEST-QA/Cadence-Ledger.md", "templates/regression/Cadence-Ledger.md"],
1233
- ["docs/01-GOVERNANCE/Lessons-SSoT.md", "templates/ssot/Lessons-SSoT.md"],
1234
- ["docs/10-WALKTHROUGH/_walkthrough-template.md", "templates/walkthrough/walkthrough-template.md"],
1235
- ["docs/10-WALKTHROUGH/Closeout-SSoT.md", "templates/walkthrough/Closeout-SSoT.md"],
1236
- ];
1237
- if (capabilities.includes("module-parallel")) {
1238
- files.push(["docs/09-PLANNING/Module-Registry.md", "templates/ssot/Module-Registry.md"]);
1239
- files.push(["docs/09-PLANNING/MODULES/_task-template/task_plan.md", "templates/planning/task_plan.md"]);
1240
- files.push(["docs/09-PLANNING/MODULES/_task-template/execution_strategy.md", "templates/planning/execution_strategy.md"]);
1241
- files.push(["docs/09-PLANNING/MODULES/_task-template/visual_roadmap.md", "templates/planning/visual_roadmap.md"]);
1242
- }
1243
- if (capabilities.includes("long-running-task")) {
1244
- files.push(["docs/09-PLANNING/TASKS/_task-template/long-running-task-contract.md", "templates/planning/long-running-task-contract.md"]);
1245
- }
1246
- return files.map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
1247
- }
1248
-
1249
- export function writeInitFiles(targetInput, capabilities, { dryRun = true, locale = "en-US" } = {}) {
1250
- const target = normalizeTarget(targetInput);
1251
- const normalizedCapabilities = [...new Set(capabilities.map(normalizeCapabilityName))];
1252
- const normalizedLocale = normalizeLocale(locale);
1253
- const existingRegistry = readCapabilityRegistry(target);
1254
- if (existingRegistry.raw) {
1255
- const installed = new Set(existingRegistry.capabilities.map((capability) => capability.name));
1256
- const requested = new Set(normalizedCapabilities);
1257
- const same =
1258
- installed.size === requested.size &&
1259
- [...installed].every((capability) => requested.has(capability));
1260
- if (!same) {
1261
- throw new Error("Existing capability registry differs from requested init capabilities; use add-capability instead.");
1262
- }
1263
- }
1264
- const planned = plannedInitFiles(normalizedCapabilities, { locale: normalizedLocale });
1265
- const changes = [];
1266
- for (const [destination, source] of planned) {
1267
- const destinationPath = path.join(target.projectRoot, destination);
1268
- const sourcePath = path.join(repoRoot, source);
1269
- const existsAlready = fs.existsSync(destinationPath);
1270
- changes.push({ destination, source, action: existsAlready ? "skip-existing" : dryRun ? "would-create" : "create" });
1271
- if (!dryRun && !existsAlready) {
1272
- fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
1273
- fs.copyFileSync(sourcePath, destinationPath);
1274
- }
1275
- }
1276
- const registry = {
1277
- version: 1,
1278
- locale: normalizedLocale,
1279
- capabilities: normalizedCapabilities.map((name) => ({ name, state: "scaffolded" })),
1280
- };
1281
- if (!dryRun) {
1282
- const registryPath = path.join(target.projectRoot, ".harness-capabilities.json");
1283
- if (!fs.existsSync(registryPath)) fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
1284
- }
1285
- const report = buildInstallReport({ target, locale: normalizedLocale, capabilities: normalizedCapabilities, changes, dryRun, operation: "init" });
1286
- return { target, capabilities: normalizedCapabilities, locale: normalizedLocale, changes, report };
1287
- }
1288
-
1289
- export function addCapability(targetInput, capabilityName, { dryRun = true, locale = "" } = {}) {
1290
- const target = normalizeTarget(targetInput);
1291
- const normalizedCapability = normalizeCapabilityName(capabilityName);
1292
- if (!capabilityDefinitions[normalizedCapability]) throw new Error(`Unknown capability: ${capabilityName}`);
1293
- const registry = readCapabilityRegistry(target);
1294
- const normalizedLocale = normalizeLocale(registry.raw ? registry.locale : locale || "en-US");
1295
- const capabilityMap = new Map(registry.capabilities.map((capability) => [capability.name, capability]));
1296
- for (const dependency of capabilityDefinitions[normalizedCapability].dependencies) {
1297
- if (!capabilityMap.has(dependency)) capabilityMap.set(dependency, { name: dependency, state: "scaffolded" });
1298
- }
1299
- if (!capabilityMap.has(normalizedCapability)) capabilityMap.set(normalizedCapability, { name: normalizedCapability, state: "scaffolded" });
1300
- const next = { version: 1, locale: normalizedLocale, capabilities: [...capabilityMap.values()] };
1301
- const scaffold = plannedInitFiles([...capabilityMap.keys()], { locale: normalizedLocale });
1302
- const changes = [];
1303
- for (const [destination, source] of scaffold) {
1304
- const destinationPath = path.join(target.projectRoot, destination);
1305
- const sourcePath = path.join(repoRoot, source);
1306
- const existsAlready = fs.existsSync(destinationPath);
1307
- changes.push({ destination, source, action: existsAlready ? "skip-existing" : dryRun ? "would-create" : "create" });
1308
- if (!dryRun && !existsAlready) {
1309
- fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
1310
- fs.copyFileSync(sourcePath, destinationPath);
1311
- }
1312
- }
1313
- if (!dryRun) {
1314
- fs.writeFileSync(path.join(target.projectRoot, ".harness-capabilities.json"), `${JSON.stringify(next, null, 2)}\n`);
1315
- }
1316
- const report = buildInstallReport({ target, locale: normalizedLocale, capabilities: [...capabilityMap.keys()], changes, dryRun, operation: "add-capability" });
1317
- return { target, dryRun, registry: next, changes, report };
1318
- }
1
+ export * from "./core-shared.mjs";
2
+ export * from "./markdown-utils.mjs";
3
+ export * from "./capability-registry.mjs";
4
+ export * from "./task-scanner.mjs";
5
+ export * from "./check-profiles.mjs";
6
+ export * from "./dashboard-data.mjs";
7
+ export * from "./dashboard-workbench.mjs";
8
+ export * from "./migration-planner.mjs";
9
+ export * from "./preset-registry.mjs";
10
+ export * from "./governance-index-generator.mjs";
11
+ export * from "./task-lifecycle.mjs";
12
+ export * from "./task-lesson-sedimentation.mjs";
13
+ export * from "./lesson-maintenance.mjs";
14
+ export * from "./task-index.mjs";
15
+ export * from "./task-tombstone-commands.mjs";