freeharness 0.4.1

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 (375) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +108 -0
  3. package/README.md +134 -0
  4. package/canonical/HARVEST.yaml +858 -0
  5. package/canonical/PROVENANCE.yaml +740 -0
  6. package/canonical/agents/README.md +95 -0
  7. package/canonical/agents/agentic-architect.md +18 -0
  8. package/canonical/agents/database-reviewer.md +41 -0
  9. package/canonical/agents/explorer-ai-engineer-analyst.md +65 -0
  10. package/canonical/agents/pr-reviewer.md +96 -0
  11. package/canonical/agents/python-reviewer.md +55 -0
  12. package/canonical/agents/report-formatter.md +19 -0
  13. package/canonical/agents/security-reviewer.md +53 -0
  14. package/canonical/agents/test-writer.md +34 -0
  15. package/canonical/commands/fh-context.md +37 -0
  16. package/canonical/evals/promptfoo.redteam.yaml +108 -0
  17. package/canonical/hooks/cost-prices.json +14 -0
  18. package/canonical/hooks/freeharness-auto-memory.mjs +158 -0
  19. package/canonical/hooks/freeharness-config-guard.mjs +162 -0
  20. package/canonical/hooks/freeharness-context-monitor.mjs +198 -0
  21. package/canonical/hooks/freeharness-converge.mjs +227 -0
  22. package/canonical/hooks/freeharness-cost-tracker.mjs +1027 -0
  23. package/canonical/hooks/freeharness-memory-guard.mjs +57 -0
  24. package/canonical/hooks/freeharness-output-pruner.mjs +333 -0
  25. package/canonical/hooks/freeharness-precompact.mjs +323 -0
  26. package/canonical/hooks/freeharness-repeat-tool.mjs +208 -0
  27. package/canonical/hooks/freeharness-security-guidance.mjs +164 -0
  28. package/canonical/hooks/freeharness-session-start +583 -0
  29. package/canonical/hooks/hooks.auto-memory.claude-code.json +16 -0
  30. package/canonical/hooks/hooks.claude-code.json +17 -0
  31. package/canonical/hooks/hooks.config-guard.claude-code.json +16 -0
  32. package/canonical/hooks/hooks.context-monitor.claude-code.json +15 -0
  33. package/canonical/hooks/hooks.converge.claude-code.json +15 -0
  34. package/canonical/hooks/hooks.cost-tracker.claude-code.json +37 -0
  35. package/canonical/hooks/hooks.memory-guard.claude-code.json +16 -0
  36. package/canonical/hooks/hooks.output-pruner.claude-code.json +16 -0
  37. package/canonical/hooks/hooks.precompact.claude-code.json +15 -0
  38. package/canonical/hooks/hooks.repeat-tool.claude-code.json +25 -0
  39. package/canonical/hooks/hooks.security-guidance.claude-code.json +16 -0
  40. package/canonical/mcp/servers.json +20 -0
  41. package/canonical/openspec/config.base.yaml +89 -0
  42. package/canonical/openspec/schema.spec-driven.yaml +277 -0
  43. package/canonical/openspec/templates/data-contract.md +36 -0
  44. package/canonical/openspec/templates/design.md +84 -0
  45. package/canonical/openspec/templates/eval-plan.md +33 -0
  46. package/canonical/openspec/templates/follow-ups.md +2 -0
  47. package/canonical/openspec/templates/proposal.md +45 -0
  48. package/canonical/openspec/templates/spec.md +42 -0
  49. package/canonical/openspec/templates/tasks.md +38 -0
  50. package/canonical/profiles/local-llamacpp.LOCAL-MODELS.md +47 -0
  51. package/canonical/profiles/local-llamacpp.json +27 -0
  52. package/canonical/profiles/local-ollama.json +20 -0
  53. package/canonical/profiles/mcp.json +91 -0
  54. package/canonical/rules/CLAUDE.md +78 -0
  55. package/canonical/rules/project-CLAUDE.template.md +62 -0
  56. package/canonical/rules/routing.md +20 -0
  57. package/canonical/rules/safety.md +41 -0
  58. package/canonical/settings/settings.cache-hygiene.claude-code.json +5 -0
  59. package/canonical/skills/agent-evaluation/SKILL.md +102 -0
  60. package/canonical/skills/agent-evaluation/assets/evaluation_report_template.md +206 -0
  61. package/canonical/skills/agent-evaluation/evals/cases.yaml +106 -0
  62. package/canonical/skills/agent-evaluation/references/dataset-preparation.md +351 -0
  63. package/canonical/skills/agent-evaluation/references/scorers-constraints.md +165 -0
  64. package/canonical/skills/agent-evaluation/references/scorers.md +431 -0
  65. package/canonical/skills/agent-evaluation/references/setup-guide.md +252 -0
  66. package/canonical/skills/agent-evaluation/references/tracing-integration.md +164 -0
  67. package/canonical/skills/agent-evaluation/references/troubleshooting.md +514 -0
  68. package/canonical/skills/agent-evaluation/scripts/analyze_results.py +462 -0
  69. package/canonical/skills/agent-evaluation/scripts/create_dataset_template.py +382 -0
  70. package/canonical/skills/agent-evaluation/scripts/list_datasets.py +273 -0
  71. package/canonical/skills/agent-evaluation/scripts/run_evaluation_template.py +309 -0
  72. package/canonical/skills/agent-evaluation/scripts/setup_mlflow.py +353 -0
  73. package/canonical/skills/agent-evaluation/scripts/utils/__init__.py +27 -0
  74. package/canonical/skills/agent-evaluation/scripts/utils/env_validation.py +123 -0
  75. package/canonical/skills/agent-evaluation/scripts/utils/tracing_utils.py +108 -0
  76. package/canonical/skills/agent-evaluation/scripts/validate_agent_tracing.py +148 -0
  77. package/canonical/skills/agent-evaluation/scripts/validate_auth.py +240 -0
  78. package/canonical/skills/agent-evaluation/scripts/validate_environment.py +185 -0
  79. package/canonical/skills/agent-evaluation/scripts/validate_tracing_runtime.py +318 -0
  80. package/canonical/skills/agentic-architecture/SKILL.md +41 -0
  81. package/canonical/skills/agentic-architecture/evals/cases.yaml +94 -0
  82. package/canonical/skills/agentic-architecture/reference/context-engineering.md +75 -0
  83. package/canonical/skills/agentic-architecture/reference/patterns.md +76 -0
  84. package/canonical/skills/agentic-architecture/reference/reliability-observability.md +94 -0
  85. package/canonical/skills/agentic-architecture/reference/review-framework.md +59 -0
  86. package/canonical/skills/code-review/SKILL.md +52 -0
  87. package/canonical/skills/code-review/evals/cases.yaml +197 -0
  88. package/canonical/skills/git-workflow/SKILL.md +41 -0
  89. package/canonical/skills/git-workflow/evals/cases.yaml +92 -0
  90. package/canonical/skills/local-code/SKILL.md +24 -0
  91. package/canonical/skills/local-code/evals/cases.yaml +85 -0
  92. package/canonical/skills/local-code/scripts/query_llama.py +68 -0
  93. package/canonical/skills/report-formatting/SKILL.md +62 -0
  94. package/canonical/skills/report-formatting/evals/cases.yaml +83 -0
  95. package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +135 -0
  96. package/canonical/skills/report-formatting/reference/HELPERS.md +120 -0
  97. package/canonical/skills/report-formatting/reference/PPTX.md +136 -0
  98. package/canonical/skills/report-formatting/reference/RELEASE.md +99 -0
  99. package/canonical/skills/report-formatting/reference/TEMPLATES.md +48 -0
  100. package/canonical/skills/report-formatting/reference/authoring.md +66 -0
  101. package/canonical/skills/report-formatting/scripts/architecture-template.md +271 -0
  102. package/canonical/skills/report-formatting/scripts/assets/claro-logo.png +0 -0
  103. package/canonical/skills/report-formatting/scripts/assets/claro-wordmark.svg +1 -0
  104. package/canonical/skills/report-formatting/scripts/build-architecture.js +57 -0
  105. package/canonical/skills/report-formatting/scripts/build-md-claro.js +224 -0
  106. package/canonical/skills/report-formatting/scripts/build-release.js +51 -0
  107. package/canonical/skills/report-formatting/scripts/check-env.js +58 -0
  108. package/canonical/skills/report-formatting/scripts/docmodel.js +283 -0
  109. package/canonical/skills/report-formatting/scripts/example-build.js +119 -0
  110. package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +195 -0
  111. package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +114 -0
  112. package/canonical/skills/report-formatting/scripts/package.json +17 -0
  113. package/canonical/skills/report-formatting/scripts/pptx/example_build_StatusReport_pptx.py +223 -0
  114. package/canonical/skills/report-formatting/scripts/pptx/example_build_WeeklyStatus_pptx.py +245 -0
  115. package/canonical/skills/report-formatting/scripts/pptx/pptx_office.py +144 -0
  116. package/canonical/skills/report-formatting/scripts/pptx/requirements.txt +7 -0
  117. package/canonical/skills/report-formatting/scripts/pptx/status_report_blocks.py +193 -0
  118. package/canonical/skills/report-formatting/scripts/pptx/theme_claro_pptx.py +473 -0
  119. package/canonical/skills/report-formatting/scripts/release-template.md +80 -0
  120. package/canonical/skills/report-formatting/scripts/render-mermaid.js +105 -0
  121. package/canonical/skills/report-formatting/scripts/theme-claro.js +403 -0
  122. package/canonical/skills/subagent-driven-development/SKILL.md +60 -0
  123. package/canonical/skills/subagent-driven-development/evals/cases.yaml +208 -0
  124. package/canonical/skills/subagent-driven-development/reference/brief.md +91 -0
  125. package/canonical/skills/subagent-driven-development/reference/handoff.md +118 -0
  126. package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +850 -0
  127. package/canonical/skills/systematic-debugging/SKILL.md +44 -0
  128. package/canonical/skills/systematic-debugging/evals/cases.yaml +90 -0
  129. package/canonical/skills/test-driven-development/SKILL.md +42 -0
  130. package/canonical/skills/test-driven-development/evals/cases.yaml +86 -0
  131. package/canonical/skills/test-driven-development/reference/writing-good-tests.md +52 -0
  132. package/canonical/skills/verification-before-completion/SKILL.md +53 -0
  133. package/canonical/skills/verification-before-completion/evals/cases.yaml +107 -0
  134. package/dist/adapters/content/language-lint.d.ts +46 -0
  135. package/dist/adapters/content/language-lint.js +191 -0
  136. package/dist/adapters/harness/claude-code.d.ts +57 -0
  137. package/dist/adapters/harness/claude-code.js +385 -0
  138. package/dist/adapters/harness/index.d.ts +3 -0
  139. package/dist/adapters/harness/index.js +17 -0
  140. package/dist/adapters/harness/opencode.d.ts +7 -0
  141. package/dist/adapters/harness/opencode.js +166 -0
  142. package/dist/adapters/harness/plugin.d.ts +22 -0
  143. package/dist/adapters/harness/plugin.js +257 -0
  144. package/dist/adapters/memory/doctor.d.ts +480 -0
  145. package/dist/adapters/memory/doctor.js +1217 -0
  146. package/dist/adapters/memory/vendored.d.ts +150 -0
  147. package/dist/adapters/memory/vendored.js +487 -0
  148. package/dist/adapters/package/node.d.ts +9 -0
  149. package/dist/adapters/package/node.js +20 -0
  150. package/dist/adapters/prompt/readline.d.ts +19 -0
  151. package/dist/adapters/prompt/readline.js +53 -0
  152. package/dist/adapters/sync/diff.d.ts +43 -0
  153. package/dist/adapters/sync/diff.js +380 -0
  154. package/dist/cli/cleanup.d.ts +86 -0
  155. package/dist/cli/cleanup.js +382 -0
  156. package/dist/cli/cost.d.ts +304 -0
  157. package/dist/cli/cost.js +501 -0
  158. package/dist/cli/doctor.d.ts +205 -0
  159. package/dist/cli/doctor.js +654 -0
  160. package/dist/cli/executor.d.ts +5 -0
  161. package/dist/cli/executor.js +48 -0
  162. package/dist/cli/plugin-validate.d.ts +37 -0
  163. package/dist/cli/plugin-validate.js +256 -0
  164. package/dist/cli/prefix.d.ts +68 -0
  165. package/dist/cli/prefix.js +218 -0
  166. package/dist/cli/profile.d.ts +170 -0
  167. package/dist/cli/profile.js +285 -0
  168. package/dist/cli/runner.d.ts +10 -0
  169. package/dist/cli/runner.js +27 -0
  170. package/dist/cli/uninstall.d.ts +65 -0
  171. package/dist/cli/uninstall.js +147 -0
  172. package/dist/index.d.ts +8 -0
  173. package/dist/index.js +989 -0
  174. package/dist/infra/dogfood.d.ts +31 -0
  175. package/dist/infra/dogfood.js +51 -0
  176. package/dist/infra/fsutil.d.ts +40 -0
  177. package/dist/infra/fsutil.js +143 -0
  178. package/dist/infra/manifest.d.ts +76 -0
  179. package/dist/infra/manifest.js +92 -0
  180. package/dist/infra/paths.d.ts +19 -0
  181. package/dist/infra/paths.js +74 -0
  182. package/dist/infra/routing.d.ts +4 -0
  183. package/dist/infra/routing.js +7 -0
  184. package/node_modules/@freeharness/core/dist/context.d.ts +30 -0
  185. package/node_modules/@freeharness/core/dist/context.js +1 -0
  186. package/node_modules/@freeharness/core/dist/index.d.ts +5 -0
  187. package/node_modules/@freeharness/core/dist/index.js +6 -0
  188. package/node_modules/@freeharness/core/dist/planner.d.ts +13 -0
  189. package/node_modules/@freeharness/core/dist/planner.js +18 -0
  190. package/node_modules/@freeharness/core/dist/ports.d.ts +90 -0
  191. package/node_modules/@freeharness/core/dist/ports.js +1 -0
  192. package/node_modules/@freeharness/core/dist/registry.d.ts +41 -0
  193. package/node_modules/@freeharness/core/dist/registry.js +124 -0
  194. package/node_modules/@freeharness/core/dist/tiers.d.ts +30 -0
  195. package/node_modules/@freeharness/core/dist/tiers.js +99 -0
  196. package/node_modules/@freeharness/core/package.json +34 -0
  197. package/node_modules/@freeharness/sdd/PROVENANCE.yaml +175 -0
  198. package/node_modules/@freeharness/sdd/dist/src/bin/openspec.d.ts +2 -0
  199. package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +166 -0
  200. package/node_modules/@freeharness/sdd/dist/src/index.d.ts +14 -0
  201. package/node_modules/@freeharness/sdd/dist/src/index.js +11 -0
  202. package/node_modules/@freeharness/sdd/dist/src/init.d.ts +39 -0
  203. package/node_modules/@freeharness/sdd/dist/src/init.js +190 -0
  204. package/node_modules/@freeharness/sdd/dist/src/root-resolver.d.ts +8 -0
  205. package/node_modules/@freeharness/sdd/dist/src/root-resolver.js +32 -0
  206. package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +81 -0
  207. package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +483 -0
  208. package/node_modules/@freeharness/sdd/dist/src/shim/context.d.ts +4 -0
  209. package/node_modules/@freeharness/sdd/dist/src/shim/context.js +48 -0
  210. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +57 -0
  211. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +279 -0
  212. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +67 -0
  213. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +460 -0
  214. package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +5 -0
  215. package/node_modules/@freeharness/sdd/dist/src/shim/list.js +95 -0
  216. package/node_modules/@freeharness/sdd/dist/src/shim/new-change.d.ts +6 -0
  217. package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +150 -0
  218. package/node_modules/@freeharness/sdd/dist/src/shim/schemas.d.ts +4 -0
  219. package/node_modules/@freeharness/sdd/dist/src/shim/schemas.js +23 -0
  220. package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +56 -0
  221. package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +140 -0
  222. package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +8 -0
  223. package/node_modules/@freeharness/sdd/dist/src/shim/status.js +85 -0
  224. package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +6 -0
  225. package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +229 -0
  226. package/node_modules/@freeharness/sdd/dist/src/validate-change.d.ts +40 -0
  227. package/node_modules/@freeharness/sdd/dist/src/validate-change.js +114 -0
  228. package/node_modules/@freeharness/sdd/dist/src/validate.d.ts +24 -0
  229. package/node_modules/@freeharness/sdd/dist/src/validate.js +72 -0
  230. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.d.ts +130 -0
  231. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +981 -0
  232. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-metadata.d.ts +46 -0
  233. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-metadata.js +128 -0
  234. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +26 -0
  235. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +32 -0
  236. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/file-system.d.ts +50 -0
  237. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/file-system.js +394 -0
  238. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/global-config.d.ts +15 -0
  239. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/global-config.js +49 -0
  240. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/graph.d.ts +71 -0
  241. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/graph.js +164 -0
  242. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +11 -0
  243. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +21 -0
  244. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/instruction-loader.d.ts +102 -0
  245. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/instruction-loader.js +230 -0
  246. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +18 -0
  247. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +138 -0
  248. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/resolver.d.ts +82 -0
  249. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/resolver.js +333 -0
  250. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/schema.d.ts +12 -0
  251. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/schema.js +110 -0
  252. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/state.d.ts +11 -0
  253. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/state.js +32 -0
  254. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.d.ts +33 -0
  255. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +149 -0
  256. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.d.ts +39 -0
  257. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +49 -0
  258. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/claude.d.ts +12 -0
  259. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/claude.js +31 -0
  260. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/index.d.ts +10 -0
  261. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/index.js +10 -0
  262. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/opencode.d.ts +12 -0
  263. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/opencode.js +26 -0
  264. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/generator.d.ts +28 -0
  265. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/generator.js +41 -0
  266. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/index.d.ts +23 -0
  267. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/index.js +25 -0
  268. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/invocation.d.ts +72 -0
  269. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/invocation.js +76 -0
  270. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/registry.d.ts +39 -0
  271. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/registry.js +53 -0
  272. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/types.d.ts +62 -0
  273. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/types.js +7 -0
  274. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/yaml.d.ts +35 -0
  275. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/yaml.js +47 -0
  276. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-references.d.ts +33 -0
  277. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-references.js +55 -0
  278. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/allowed-tools.d.ts +11 -0
  279. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/allowed-tools.js +11 -0
  280. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/skill-generation.d.ts +48 -0
  281. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/skill-generation.js +99 -0
  282. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/skill-templates.d.ts +19 -0
  283. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/skill-templates.js +18 -0
  284. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/types.d.ts +18 -0
  285. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/types.js +4 -0
  286. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.d.ts +19 -0
  287. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +207 -0
  288. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.d.ts +12 -0
  289. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +409 -0
  290. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/bulk-archive-change.d.ts +9 -0
  291. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/bulk-archive-change.js +673 -0
  292. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/continue-change.d.ts +9 -0
  293. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/continue-change.js +230 -0
  294. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.d.ts +12 -0
  295. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +550 -0
  296. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/feedback.d.ts +8 -0
  297. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/feedback.js +107 -0
  298. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/ff-change.d.ts +12 -0
  299. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/ff-change.js +228 -0
  300. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/new-change.d.ts +9 -0
  301. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/new-change.js +147 -0
  302. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/onboard.d.ts +9 -0
  303. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/onboard.js +571 -0
  304. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.d.ts +12 -0
  305. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +322 -0
  306. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/store-selection.d.ts +7 -0
  307. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/store-selection.js +7 -0
  308. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.d.ts +9 -0
  309. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +518 -0
  310. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.d.ts +9 -0
  311. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +176 -0
  312. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/verify-change.d.ts +9 -0
  313. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/verify-change.js +344 -0
  314. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +39 -0
  315. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +54 -0
  316. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/delta-resolution.d.ts +35 -0
  317. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/delta-resolution.js +171 -0
  318. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +27 -0
  319. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +210 -0
  320. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/code-fence.d.ts +14 -0
  321. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/code-fence.js +48 -0
  322. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.d.ts +22 -0
  323. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +186 -0
  324. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +65 -0
  325. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +409 -0
  326. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +55 -0
  327. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +98 -0
  328. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.d.ts +8 -0
  329. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +77 -0
  330. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.d.ts +44 -0
  331. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +116 -0
  332. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/base.schema.d.ts +12 -0
  333. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/base.schema.js +17 -0
  334. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/change.schema.d.ts +72 -0
  335. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/change.schema.js +30 -0
  336. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/index.d.ts +3 -0
  337. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/index.js +3 -0
  338. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/spec.schema.d.ts +17 -0
  339. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/spec.schema.js +14 -0
  340. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +39 -0
  341. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +114 -0
  342. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +45 -0
  343. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +105 -0
  344. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-numbering.d.ts +15 -0
  345. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-numbering.js +56 -0
  346. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/types.d.ts +17 -0
  347. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/types.js +1 -0
  348. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +94 -0
  349. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +756 -0
  350. package/node_modules/@freeharness/sdd/package.json +57 -0
  351. package/node_modules/@freeharness/sdd/vendor/openspec/LICENSE.upstream +22 -0
  352. package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +232 -0
  353. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/README.md +19 -0
  354. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +187 -0
  355. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +181 -0
  356. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +338 -0
  357. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +117 -0
  358. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +342 -0
  359. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +116 -0
  360. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +76 -0
  361. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +560 -0
  362. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +162 -0
  363. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +261 -0
  364. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +90 -0
  365. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +174 -0
  366. package/package.json +70 -0
  367. package/provenance/canonical.PROVENANCE.yaml +740 -0
  368. package/provenance/claude-mem.LICENSE.upstream +202 -0
  369. package/provenance/memory.PROVENANCE.yaml +399 -0
  370. package/provenance/sdd.PROVENANCE.yaml +175 -0
  371. package/vendor-licenses/caveman.LICENSE +27 -0
  372. package/vendor-licenses/deepseek-harness.LICENSE +21 -0
  373. package/vendor-licenses/ecc.LICENSE +21 -0
  374. package/vendor-licenses/pi.LICENSE +21 -0
  375. package/vendor-licenses/superpowers.LICENSE +21 -0
@@ -0,0 +1,756 @@
1
+ import { readFileSync, promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { SpecSchema, ChangeSchema } from './schemas/index.js';
4
+ import { MarkdownParser } from './parsers/markdown-parser.js';
5
+ import { ChangeParser } from './parsers/change-parser.js';
6
+ import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
7
+ import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, findMissingCurrentScenarios, } from './parsers/requirement-blocks.js';
8
+ import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, } from './parsers/requirement-text.js';
9
+ import { findMainSpecStructureIssues } from './parsers/spec-structure.js';
10
+ import { FileSystemUtils } from '../engine/file-system.js';
11
+ import { discoverSpecFiles } from './spec-discovery.js';
12
+ import { findTaskNumberingIssues } from './task-numbering.js';
13
+ import { findPurposePlaceholderIssue } from './purpose-placeholder.js';
14
+ import { findSpecUpdates, buildUpdatedSpec } from '../apply/specs-apply.js';
15
+ import { findUnresolvedDeltaHeaders } from './delta-resolution.js';
16
+ export class Validator {
17
+ strictMode;
18
+ constructor(strictMode = false) {
19
+ this.strictMode = strictMode;
20
+ }
21
+ async validateSpec(filePath) {
22
+ const issues = [];
23
+ const specName = this.extractNameFromPath(filePath);
24
+ try {
25
+ const content = readFileSync(filePath, 'utf-8');
26
+ const parser = new MarkdownParser(content);
27
+ const spec = parser.parseSpec(specName);
28
+ const result = SpecSchema.safeParse(spec);
29
+ if (!result.success) {
30
+ issues.push(...this.convertZodErrors(result.error));
31
+ }
32
+ issues.push(...this.applySpecRules(spec, content));
33
+ }
34
+ catch (error) {
35
+ const baseMessage = error instanceof Error ? error.message : 'Unknown error';
36
+ const enriched = this.enrichTopLevelError(specName, baseMessage);
37
+ issues.push({
38
+ level: 'ERROR',
39
+ path: 'file',
40
+ message: enriched,
41
+ });
42
+ }
43
+ return this.createReport(issues);
44
+ }
45
+ /**
46
+ * Validate spec content from a string (used for pre-write validation of rebuilt specs)
47
+ */
48
+ async validateSpecContent(specName, content) {
49
+ const issues = [];
50
+ try {
51
+ const parser = new MarkdownParser(content);
52
+ const spec = parser.parseSpec(specName);
53
+ const result = SpecSchema.safeParse(spec);
54
+ if (!result.success) {
55
+ issues.push(...this.convertZodErrors(result.error));
56
+ }
57
+ issues.push(...this.applySpecRules(spec, content));
58
+ }
59
+ catch (error) {
60
+ const baseMessage = error instanceof Error ? error.message : 'Unknown error';
61
+ const enriched = this.enrichTopLevelError(specName, baseMessage);
62
+ issues.push({ level: 'ERROR', path: 'file', message: enriched });
63
+ }
64
+ return this.createReport(issues);
65
+ }
66
+ /**
67
+ * Validate the change proposal document (Why / What Changes prose + the
68
+ * top-level ChangeSchema). Delta-level checks live in
69
+ * `validateChangeDeltaSpecs`. This is the non-blocking proposal-level pass
70
+ * archive uses; TRIMMED here to drop the skip_specs marker awareness.
71
+ */
72
+ async validateChange(filePath) {
73
+ const issues = [];
74
+ const changeName = this.extractNameFromPath(filePath);
75
+ try {
76
+ const content = readFileSync(filePath, 'utf-8');
77
+ const changeDir = path.dirname(filePath);
78
+ const parser = new ChangeParser(content, changeDir);
79
+ const change = await parser.parseChangeWithDeltas(changeName);
80
+ const result = ChangeSchema.safeParse(change);
81
+ if (!result.success) {
82
+ issues.push(...this.convertZodErrors(result.error));
83
+ }
84
+ issues.push(...this.applyChangeRules(change, content));
85
+ }
86
+ catch (error) {
87
+ const baseMessage = error instanceof Error ? error.message : 'Unknown error';
88
+ const enriched = this.enrichTopLevelError(changeName, baseMessage);
89
+ issues.push({
90
+ level: 'ERROR',
91
+ path: 'file',
92
+ message: enriched,
93
+ });
94
+ }
95
+ return this.createReport(issues);
96
+ }
97
+ /**
98
+ * Validate delta-formatted spec files under a change directory.
99
+ * Enforces:
100
+ * - At least one delta across all files
101
+ * - ADDED/MODIFIED: each requirement has at least one scenario; missing
102
+ * English SHALL/MUST keywords are guidance unless strict mode is enabled
103
+ * - REMOVED: names only; no scenario/description required
104
+ * - RENAMED: pairs well-formed
105
+ * - No duplicates within sections; no cross-section conflicts per spec
106
+ *
107
+ * When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
108
+ * against the current main specs for the scenario loss archive refuses to
109
+ * apply (#1477). When `options.projectRoot` is given, the change's tasks.md is
110
+ * checked for ambiguous task numbering (#1520).
111
+ */
112
+ async validateChangeDeltaSpecs(changeDir, options = {}) {
113
+ const issues = [];
114
+ const specsDir = path.join(changeDir, 'specs');
115
+ let totalDeltas = 0;
116
+ let hasRootLevelSpec = false;
117
+ const missingHeaderSpecs = [];
118
+ const emptySectionSpecs = [];
119
+ try {
120
+ // Discover delta specs through the same helper the change parser, show,
121
+ // apply, and archive use, so validate never accepts a layout the merge
122
+ // path silently skips (#1385). It finds spec.md at any depth, covering
123
+ // both specs/<capability>/spec.md and the nested multi-area
124
+ // specs/<area>/<capability>/spec.md layout (#1182b).
125
+ const discoveredSpecs = await discoverSpecFiles(specsDir);
126
+ // A spec.md directly at the specs/ root has no capability folder, so the
127
+ // merge path drops it: without this error the change validates clean and
128
+ // archives while its requirements never reach openspec/specs/ (#1385).
129
+ // Only a regular file counts — a *directory* named spec.md is a capability
130
+ // folder like any other, and discoverSpecFiles reads it normally.
131
+ const rootSpecStat = await fs.stat(path.join(specsDir, 'spec.md')).catch(() => null);
132
+ hasRootLevelSpec = rootSpecStat?.isFile() === true;
133
+ if (hasRootLevelSpec) {
134
+ issues.push({
135
+ level: 'ERROR',
136
+ path: 'spec.md',
137
+ message: 'Delta spec found at specs/spec.md. Delta specs must live under a capability path (e.g. specs/<capability-path>/spec.md) — a file at the specs/ root is ignored when the change is applied or archived.',
138
+ });
139
+ }
140
+ for (const { id: specId, specFile } of discoveredSpecs) {
141
+ let content;
142
+ try {
143
+ content = await fs.readFile(specFile, 'utf-8');
144
+ }
145
+ catch {
146
+ continue;
147
+ }
148
+ const plan = parseDeltaSpec(content);
149
+ const entryPath = FileSystemUtils.toPosixPath(path.relative(specsDir, specFile));
150
+ // Surface (as INFO, never a failure) the non-canonical level-3 headers
151
+ // the delta reader skipped while parsing ADDED/MODIFIED sections —
152
+ // without this note a stray divider like "### Documentation
153
+ // Requirements" would pass validate <change> while failing
154
+ // archive/validate <spec>. The list comes from the parse itself, so it
155
+ // reflects exactly what the reader skipped.
156
+ for (const stray of plan.skippedHeaders) {
157
+ const nameless = /^requirement:?$/i.test(stray.header);
158
+ issues.push({
159
+ level: 'INFO',
160
+ path: entryPath,
161
+ line: stray.line,
162
+ message: nameless
163
+ ? `Header "### ${stray.header}" in ${stray.section} is missing a requirement name and is ignored by validation. Add a name, e.g. "### Requirement: <name>".`
164
+ : `Header "### ${stray.header}" in ${stray.section} is not a "### Requirement:" header and is ignored by validation. Use "### Requirement: ${stray.header}" if it should be validated as a requirement.`,
165
+ });
166
+ }
167
+ const sectionNames = [];
168
+ if (plan.sectionPresence.added)
169
+ sectionNames.push('## ADDED Requirements');
170
+ if (plan.sectionPresence.modified)
171
+ sectionNames.push('## MODIFIED Requirements');
172
+ if (plan.sectionPresence.removed)
173
+ sectionNames.push('## REMOVED Requirements');
174
+ if (plan.sectionPresence.renamed)
175
+ sectionNames.push('## RENAMED Requirements');
176
+ const hasSections = sectionNames.length > 0;
177
+ const hasEntries = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
178
+ if (!hasEntries) {
179
+ if (hasSections)
180
+ emptySectionSpecs.push({ path: entryPath, sections: sectionNames });
181
+ else
182
+ missingHeaderSpecs.push(entryPath);
183
+ }
184
+ const addedNames = new Set();
185
+ const modifiedNames = new Set();
186
+ const removedNames = new Set();
187
+ const renamedFrom = new Set();
188
+ const renamedTo = new Set();
189
+ // Validate ADDED
190
+ for (const block of plan.added) {
191
+ const key = normalizeRequirementName(block.name);
192
+ totalDeltas++;
193
+ if (addedNames.has(key)) {
194
+ issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in ADDED: "${block.name}"` });
195
+ }
196
+ else {
197
+ addedNames.add(key);
198
+ }
199
+ const requirementText = this.extractRequirementText(block.raw);
200
+ if (!requirementText) {
201
+ issues.push({
202
+ level: 'ERROR',
203
+ path: entryPath,
204
+ message: this.containsShallOrMust(block.name)
205
+ ? this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name)
206
+ : `ADDED "${block.name}" is missing requirement text`,
207
+ });
208
+ }
209
+ else if (!this.containsShallOrMust(requirementText)) {
210
+ issues.push({
211
+ level: 'WARNING',
212
+ path: entryPath,
213
+ message: this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name, true),
214
+ });
215
+ }
216
+ const scenarioCount = this.countScenarios(block.raw);
217
+ if (scenarioCount < 1) {
218
+ issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario` });
219
+ }
220
+ }
221
+ // Validate MODIFIED
222
+ for (const block of plan.modified) {
223
+ const key = normalizeRequirementName(block.name);
224
+ totalDeltas++;
225
+ if (modifiedNames.has(key)) {
226
+ issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in MODIFIED: "${block.name}"` });
227
+ }
228
+ else {
229
+ modifiedNames.add(key);
230
+ }
231
+ const requirementText = this.extractRequirementText(block.raw);
232
+ if (!requirementText) {
233
+ issues.push({
234
+ level: 'ERROR',
235
+ path: entryPath,
236
+ message: this.containsShallOrMust(block.name)
237
+ ? this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name)
238
+ : `MODIFIED "${block.name}" is missing requirement text`,
239
+ });
240
+ }
241
+ else if (!this.containsShallOrMust(requirementText)) {
242
+ issues.push({
243
+ level: 'WARNING',
244
+ path: entryPath,
245
+ message: this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name, true),
246
+ });
247
+ }
248
+ const scenarioCount = this.countScenarios(block.raw);
249
+ if (scenarioCount < 1) {
250
+ issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario` });
251
+ }
252
+ }
253
+ // Run archive's scenario-loss check here too, so the change fails at
254
+ // authoring time instead of days later at archive time (#1477).
255
+ if (options.mainSpecsDir && plan.modified.length > 0) {
256
+ const mainSpecFile = path.join(options.mainSpecsDir, ...specId.split('/'), 'spec.md');
257
+ FileSystemUtils.assertPathWithin(path.dirname(mainSpecFile), mainSpecFile);
258
+ issues.push(...(await this.findScenarioLossIssues(plan.modified, plan.renamed, mainSpecFile, entryPath, path.dirname(mainSpecFile))));
259
+ }
260
+ // Validate REMOVED (names only)
261
+ for (const name of plan.removed) {
262
+ const key = normalizeRequirementName(name);
263
+ totalDeltas++;
264
+ if (removedNames.has(key)) {
265
+ issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in REMOVED: "${name}"` });
266
+ }
267
+ else {
268
+ removedNames.add(key);
269
+ }
270
+ }
271
+ // Validate RENAMED pairs
272
+ for (const { from, to } of plan.renamed) {
273
+ const fromKey = normalizeRequirementName(from);
274
+ const toKey = normalizeRequirementName(to);
275
+ totalDeltas++;
276
+ if (renamedFrom.has(fromKey)) {
277
+ issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate FROM in RENAMED: "${from}"` });
278
+ }
279
+ else {
280
+ renamedFrom.add(fromKey);
281
+ }
282
+ if (renamedTo.has(toKey)) {
283
+ issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate TO in RENAMED: "${to}"` });
284
+ }
285
+ else {
286
+ renamedTo.add(toKey);
287
+ }
288
+ }
289
+ // Cross-section conflicts (within the same spec file)
290
+ for (const n of modifiedNames) {
291
+ if (removedNames.has(n)) {
292
+ issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and REMOVED: "${n}"` });
293
+ }
294
+ if (addedNames.has(n)) {
295
+ issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and ADDED: "${n}"` });
296
+ }
297
+ }
298
+ for (const n of addedNames) {
299
+ if (removedNames.has(n)) {
300
+ issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both ADDED and REMOVED: "${n}"` });
301
+ }
302
+ }
303
+ for (const { from, to } of plan.renamed) {
304
+ const fromKey = normalizeRequirementName(from);
305
+ const toKey = normalizeRequirementName(to);
306
+ if (modifiedNames.has(fromKey)) {
307
+ issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED references old name from RENAMED. Use new header for "${to}"` });
308
+ }
309
+ if (addedNames.has(toKey)) {
310
+ issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
311
+ }
312
+ // Folded comparison: a case/whitespace variant of the FROM header
313
+ // in REMOVED is the same contradiction, not a different name.
314
+ const removedFoldMatch = [...removedNames].find((r) => foldRequirementName(r) === foldRequirementName(fromKey));
315
+ if (removedFoldMatch !== undefined) {
316
+ issues.push({
317
+ level: 'ERROR',
318
+ path: entryPath,
319
+ message: `Requirement present in both RENAMED and REMOVED: "${from}"` +
320
+ (removedFoldMatch === fromKey ? '' : ` (REMOVED spells it "${removedFoldMatch}")`),
321
+ });
322
+ }
323
+ }
324
+ }
325
+ // Reuse archive's merge builder to report conflicts with the main specs.
326
+ // Keep structural errors and scenario loss in their existing diagnostics.
327
+ if (options.mainSpecsDir) {
328
+ issues.push(...(await this.findArchiveBlockers(changeDir, options.mainSpecsDir, [
329
+ ...issues.filter((issue) => issue.level === 'ERROR').map((issue) => issue.path),
330
+ // Collected in the loop above but not turned into issues until
331
+ // after this try block, so they are invisible to the filter. A
332
+ // delta with no parsed sections has nothing for the merge to
333
+ // apply, which it reports as a failure of its own - on top of the
334
+ // error that actually names the mistake.
335
+ ...missingHeaderSpecs,
336
+ ...emptySectionSpecs.map((spec) => spec.path),
337
+ ])));
338
+ issues.push(...(await this.findUnresolvedHeaderIssues(changeDir, options.mainSpecsDir)));
339
+ }
340
+ }
341
+ catch (error) {
342
+ // A missing specs dir (or a stray `specs` file) means no deltas;
343
+ // anything else (EACCES, EIO) must stay loud — discoverSpecFiles
344
+ // documents that silently dropping an unreadable capability recreates
345
+ // the data-loss class it prevents, and archive lets the same error
346
+ // propagate.
347
+ const code = error?.code;
348
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
349
+ throw error;
350
+ }
351
+ }
352
+ for (const { path: specPath, sections } of emptySectionSpecs) {
353
+ issues.push({
354
+ level: 'ERROR',
355
+ path: specPath,
356
+ message: `Delta sections ${this.formatSectionList(sections)} were found, but no requirement entries parsed. Ensure each section includes at least one "### Requirement:" block (REMOVED may use bullet list syntax).`,
357
+ });
358
+ }
359
+ for (const path of missingHeaderSpecs) {
360
+ issues.push({
361
+ level: 'ERROR',
362
+ path,
363
+ message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
364
+ });
365
+ }
366
+ // The root-level error already names the file and the fix; adding "No
367
+ // deltas found" on top would contradict it, since the deltas are sitting in
368
+ // the file just reported. (skip_specs marker handling is intentionally not
369
+ // vendored in this slice — see the file header.)
370
+ if (totalDeltas === 0 && !hasRootLevelSpec) {
371
+ issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
372
+ }
373
+ if (options.projectRoot) {
374
+ issues.push(...await this.collectTaskNumberingIssues(changeDir));
375
+ }
376
+ return this.createReport(issues);
377
+ }
378
+ /**
379
+ * Task-numbering check, reconstructed minimally for this slice. Upstream
380
+ * resolves the schema's tracked task files (artifact graph) and gates the
381
+ * check to the built-in spec-driven schema; here we read the change's
382
+ * top-level tasks.md — the spec-driven schema's `apply.tracks` target — and
383
+ * run the identical `findTaskNumberingIssues` analysis.
384
+ */
385
+ async collectTaskNumberingIssues(changeDir) {
386
+ const taskFile = path.join(changeDir, 'tasks.md');
387
+ let content;
388
+ try {
389
+ content = await fs.readFile(taskFile, 'utf-8');
390
+ }
391
+ catch {
392
+ return [];
393
+ }
394
+ return findTaskNumberingIssues([{ path: 'tasks.md', content }]).map((issue) => ({
395
+ level: 'WARNING',
396
+ path: issue.path,
397
+ line: issue.line,
398
+ message: issue.message,
399
+ }));
400
+ }
401
+ /**
402
+ * Report MODIFIED requirements whose block omits a scenario the main spec
403
+ * still carries. Uses the same comparison archive applies, so validate can
404
+ * only report what archive would refuse.
405
+ *
406
+ * Silent when the main spec or the requirement header is absent: applying a
407
+ * MODIFIED against a base that is not there yet is a different failure (a
408
+ * sister change still in flight is the legitimate case), and archive is the
409
+ * gate for it. A spec that exists but cannot be read is not absent, though —
410
+ * archive aborts on it, so reporting it beats calling the change valid.
411
+ */
412
+ async findScenarioLossIssues(modified, renamed, mainSpecFile, entryPath, mainSpecRoot) {
413
+ let mainContent;
414
+ FileSystemUtils.assertPathWithin(mainSpecRoot, mainSpecFile);
415
+ try {
416
+ mainContent = await fs.readFile(mainSpecFile, 'utf-8');
417
+ }
418
+ catch (error) {
419
+ const code = error?.code;
420
+ // Reported only for the codes that mean the file itself is unusable, and
421
+ // will be just as unusable when archive reads it. Everything else -
422
+ // ENOENT/ENOTDIR ("no main spec"), and transient resource errors like
423
+ // EMFILE that say nothing about the file - stays silent rather than
424
+ // failing a change that is fine. `validate --all` reads six changes at
425
+ // once, so a resource error must never become a verdict.
426
+ const UNUSABLE = new Set(['EACCES', 'EPERM', 'EISDIR', 'ELOOP', 'ENAMETOOLONG']);
427
+ if (!code || !UNUSABLE.has(code))
428
+ return [];
429
+ return [
430
+ {
431
+ level: 'ERROR',
432
+ path: entryPath,
433
+ message: `Could not read ${FileSystemUtils.toPosixPath(mainSpecFile)} to check the MODIFIED requirements against it ` +
434
+ `(${code}). Archive reads the same file, so fix the file before archiving.`,
435
+ },
436
+ ];
437
+ }
438
+ const currentBlocks = new Map();
439
+ for (const block of extractRequirementsSection(mainContent).bodyBlocks) {
440
+ currentBlocks.set(normalizeRequirementName(block.name), block);
441
+ }
442
+ // Archive applies RENAMED before MODIFIED, so a MODIFIED naming the new
443
+ // header is compared against the renamed block's scenarios. Fall back to
444
+ // the old header, or a rename-plus-modify pair would skip the check.
445
+ const renamedFrom = new Map(renamed.map(({ from, to }) => [normalizeRequirementName(to), normalizeRequirementName(from)]));
446
+ // Walked, not looked up once: renames chain (A→B then B→C leaves C holding
447
+ // A's block), and the visited set stops a cycle from looping forever. Every
448
+ // name in a rename cycle is also a rename FROM, so the skip above already
449
+ // keeps the walk out of one; the guard stays because the cost of being
450
+ // wrong about that is a hung CLI, not a wrong message.
451
+ const currentBlockFor = (name) => {
452
+ const visited = new Set();
453
+ let key = name;
454
+ while (key !== undefined && !visited.has(key)) {
455
+ const block = currentBlocks.get(key);
456
+ if (block)
457
+ return block;
458
+ visited.add(key);
459
+ key = renamedFrom.get(key);
460
+ }
461
+ return undefined;
462
+ };
463
+ // A MODIFIED naming a header the same delta renames away is already
464
+ // reported ("MODIFIED references old name from RENAMED"), and the block it
465
+ // would land on is not the one it names — so any scenario named here would
466
+ // send the author after the wrong requirement.
467
+ const renamedAway = new Set(renamed.map(({ from }) => normalizeRequirementName(from)));
468
+ const issues = [];
469
+ for (const block of modified) {
470
+ const key = normalizeRequirementName(block.name);
471
+ if (renamedAway.has(key))
472
+ continue;
473
+ const current = currentBlockFor(key);
474
+ if (!current)
475
+ continue;
476
+ const missing = findMissingCurrentScenarios(current, block);
477
+ if (missing.length === 0)
478
+ continue;
479
+ issues.push({
480
+ level: 'ERROR',
481
+ path: entryPath,
482
+ message: `MODIFIED "${block.name}" omits scenario(s) the current spec still has: ` +
483
+ `${missing.map(name => `"${name}"`).join(', ')}. ` +
484
+ 'Copy them into the MODIFIED block (a MODIFIED requirement replaces the whole block, so archive refuses to drop them).',
485
+ });
486
+ }
487
+ return issues;
488
+ }
489
+ convertZodErrors(error) {
490
+ return error.issues.map(err => {
491
+ let message = err.message;
492
+ if (message === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
493
+ message = `${message}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
494
+ }
495
+ return {
496
+ level: 'ERROR',
497
+ path: err.path.join('.'),
498
+ message,
499
+ };
500
+ });
501
+ }
502
+ applySpecRules(spec, content) {
503
+ const issues = [];
504
+ for (const structuralIssue of findMainSpecStructureIssues(content)) {
505
+ issues.push({
506
+ level: 'ERROR',
507
+ path: 'file',
508
+ line: structuralIssue.line,
509
+ message: structuralIssue.message,
510
+ });
511
+ }
512
+ // The placeholder is longer than MIN_PURPOSE_LENGTH, so the brevity check
513
+ // below cannot reach it; it is reported on its own terms instead. Checked
514
+ // first because a hand-written "TBD" is both a placeholder and too brief,
515
+ // and only one of those two tells the author what to do. (A "TODO" opening
516
+ // the Purpose reads the same way, so it is the same finding.)
517
+ const placeholder = findPurposePlaceholderIssue(spec.overview, content);
518
+ if (placeholder) {
519
+ issues.push({
520
+ level: 'WARNING',
521
+ path: 'overview',
522
+ line: placeholder.line,
523
+ message: VALIDATION_MESSAGES.PURPOSE_IS_PLACEHOLDER,
524
+ });
525
+ }
526
+ else if (spec.overview.length < MIN_PURPOSE_LENGTH) {
527
+ issues.push({
528
+ level: 'WARNING',
529
+ path: 'overview',
530
+ message: VALIDATION_MESSAGES.PURPOSE_TOO_BRIEF,
531
+ });
532
+ }
533
+ spec.requirements.forEach((req, index) => {
534
+ if (req.text.length > MAX_REQUIREMENT_TEXT_LENGTH) {
535
+ issues.push({
536
+ level: 'INFO',
537
+ path: `requirements[${index}]`,
538
+ message: VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG,
539
+ });
540
+ }
541
+ if (req.scenarios.length === 0) {
542
+ issues.push({
543
+ level: 'WARNING',
544
+ path: `requirements[${index}].scenarios`,
545
+ message: `${VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS}. ${VALIDATION_MESSAGES.GUIDE_SCENARIO_FORMAT}`,
546
+ });
547
+ }
548
+ });
549
+ // SHALL/MUST body-keyword guidance for main specs (#1156, #243). The main-spec
550
+ // parser collapses the requirement header into `text`, so we recover the
551
+ // header+body pairs here (the same source the delta path trusts) and reuse
552
+ // the delta detection. A non-empty body that omits the English keyword gets
553
+ // guidance, while a missing body remains an error. Emitted exactly once per
554
+ // requirement (the Zod refine that used to emit a generic error is removed).
555
+ extractRequirementsSection(content).bodyBlocks.forEach((block, index) => {
556
+ const requirementText = this.extractRequirementText(block.raw);
557
+ if (!requirementText) {
558
+ issues.push({
559
+ level: 'ERROR',
560
+ path: `requirements[${index}]`,
561
+ message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name),
562
+ });
563
+ }
564
+ else if (!this.containsShallOrMust(requirementText)) {
565
+ issues.push({
566
+ level: 'WARNING',
567
+ path: `requirements[${index}]`,
568
+ message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name, true),
569
+ });
570
+ }
571
+ });
572
+ return issues;
573
+ }
574
+ applyChangeRules(change, content) {
575
+ const issues = [];
576
+ const MIN_DELTA_DESCRIPTION_LENGTH = 10;
577
+ change.deltas.forEach((delta, index) => {
578
+ if (!delta.description || delta.description.length < MIN_DELTA_DESCRIPTION_LENGTH) {
579
+ issues.push({
580
+ level: 'WARNING',
581
+ path: `deltas[${index}].description`,
582
+ message: VALIDATION_MESSAGES.DELTA_DESCRIPTION_TOO_BRIEF,
583
+ });
584
+ }
585
+ if ((delta.operation === 'ADDED' || delta.operation === 'MODIFIED') &&
586
+ (!delta.requirements || delta.requirements.length === 0)) {
587
+ issues.push({
588
+ level: 'WARNING',
589
+ path: `deltas[${index}].requirements`,
590
+ message: `${delta.operation} ${VALIDATION_MESSAGES.DELTA_MISSING_REQUIREMENTS}`,
591
+ });
592
+ }
593
+ });
594
+ return issues;
595
+ }
596
+ enrichTopLevelError(itemId, baseMessage) {
597
+ const msg = baseMessage.trim();
598
+ if (msg === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
599
+ return `${msg}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
600
+ }
601
+ if (msg.includes('Spec must have a Purpose section') || msg.includes('Spec must have a Requirements section')) {
602
+ return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_SPEC_SECTIONS}`;
603
+ }
604
+ if (msg.includes('Change must have a Why section') || msg.includes('Change must have a What Changes section')) {
605
+ return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_CHANGE_SECTIONS}`;
606
+ }
607
+ return msg;
608
+ }
609
+ extractNameFromPath(filePath) {
610
+ const normalizedPath = FileSystemUtils.toPosixPath(filePath);
611
+ const parts = normalizedPath.split('/');
612
+ // Look for the directory name after 'specs' or 'changes'
613
+ for (let i = parts.length - 1; i >= 0; i--) {
614
+ if (parts[i] === 'specs' || parts[i] === 'changes') {
615
+ if (i < parts.length - 1) {
616
+ return parts[i + 1];
617
+ }
618
+ }
619
+ }
620
+ // Fallback to filename without extension if not in expected structure
621
+ const fileName = parts[parts.length - 1] ?? '';
622
+ const dotIndex = fileName.lastIndexOf('.');
623
+ return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
624
+ }
625
+ /**
626
+ * Dry-run archive's merge builder without writing its result. Reusing the
627
+ * builder preserves its already-synced delta rules instead of duplicating them.
628
+ * INFO leaves the verdict unchanged: a missing target can be a typo or a
629
+ * requirement introduced by a sibling change that has not archived yet.
630
+ * This does not run archive's later merged-spec validation or retirement checks.
631
+ */
632
+ async findArchiveBlockers(changeDir, mainSpecsDir, alreadyReportedPaths) {
633
+ const alreadyReported = new Set(alreadyReportedPaths);
634
+ // Only ever reaches a generated skeleton's placeholder Purpose, which this
635
+ // dry run discards.
636
+ const changeName = path.basename(changeDir);
637
+ const issues = [];
638
+ let updates;
639
+ try {
640
+ updates = await findSpecUpdates(changeDir, mainSpecsDir);
641
+ }
642
+ catch (error) {
643
+ // An incomplete advisory check must not discard the validation report.
644
+ // Source discovery already ran above; archive retains its own path guards.
645
+ return [{
646
+ level: 'INFO',
647
+ path: 'specs',
648
+ message: `Could not check archive merge conflicts: ${error instanceof Error ? error.message : String(error)}`,
649
+ }];
650
+ }
651
+ for (const update of updates) {
652
+ // discoverSpecFiles builds both this id and the entryPath the checks
653
+ // above report under, from the same walk.
654
+ const entryPath = FileSystemUtils.toPosixPath(`${update.id}/spec.md`);
655
+ // A delta those checks already rejected would be reported twice, the
656
+ // second time in archive's wording rather than the wording that names
657
+ // the actual mistake.
658
+ if (alreadyReported.has(entryPath))
659
+ continue;
660
+ try {
661
+ await buildUpdatedSpec(update, changeName, { silent: true });
662
+ }
663
+ catch (error) {
664
+ // Only the thrown preconditions, which carry no errno. A filesystem
665
+ // error says nothing about whether the delta applies, and `validate
666
+ // --all` reads six changes at once, so a transient EMFILE would report
667
+ // a collision that is not there - the same reason the scenario-loss
668
+ // check above reads only the codes that mean the file is unusable.
669
+ if (error?.code !== undefined)
670
+ continue;
671
+ issues.push({
672
+ level: 'INFO',
673
+ path: entryPath,
674
+ message: `Archive would refuse this delta: ${error instanceof Error ? error.message : String(error)}`,
675
+ });
676
+ }
677
+ }
678
+ return issues;
679
+ }
680
+ /**
681
+ * FreeHarness addition: MODIFIED / REMOVED / RENAMED headers absent from the
682
+ * current main spec (delta-resolution.ts). ERROR in strict mode, WARNING
683
+ * otherwise: the default verdict tolerates a sister change still in flight,
684
+ * strict does not. The change's parent directory is the changes root, which
685
+ * is where the ADDED-elsewhere attribution looks.
686
+ */
687
+ async findUnresolvedHeaderIssues(changeDir, mainSpecsDir) {
688
+ const level = this.strictMode ? 'ERROR' : 'WARNING';
689
+ const unresolved = await findUnresolvedDeltaHeaders(changeDir, mainSpecsDir, path.dirname(changeDir));
690
+ return unresolved.map((u) => ({ level, path: u.entryPath, message: u.message }));
691
+ }
692
+ createReport(issues) {
693
+ const errors = issues.filter(i => i.level === 'ERROR').length;
694
+ const warnings = issues.filter(i => i.level === 'WARNING').length;
695
+ const info = issues.filter(i => i.level === 'INFO').length;
696
+ const valid = this.strictMode
697
+ ? errors === 0 && warnings === 0
698
+ : errors === 0;
699
+ return {
700
+ valid,
701
+ issues,
702
+ summary: {
703
+ errors,
704
+ warnings,
705
+ info,
706
+ },
707
+ };
708
+ }
709
+ isValid(report) {
710
+ return report.valid;
711
+ }
712
+ extractRequirementText(blockRaw) {
713
+ // Delegate to the shared, fence-/metadata-/multi-line-aware body reader.
714
+ // Validation intentionally does not use the parser/display header-title
715
+ // fallback for canonical `### Requirement:` blocks: #1280 requires a
716
+ // SHALL/MUST that appears only in the header to receive the body-keyword
717
+ // hint. Line 0 is the `### Requirement: ...` header.
718
+ const [, ...bodyLines] = blockRaw.split('\n');
719
+ return extractRequirementBodyShared(bodyLines) || undefined;
720
+ }
721
+ containsShallOrMust(text) {
722
+ return containsShallOrMustShared(text);
723
+ }
724
+ /**
725
+ * Build a message for a requirement block whose body lacks SHALL/MUST.
726
+ *
727
+ * When the SHALL/MUST keyword already appears in the requirement header (e.g.
728
+ * `### Requirement: The system SHALL ...`) the original generic error
729
+ * ("must contain SHALL or MUST") is confusing because the keyword is visibly
730
+ * present in the spec. Per the OpenSpec conventions the keyword has to live
731
+ * on the requirement body line (the line right after the header), so we point
732
+ * the author at that exact fix when the keyword is found in the header only.
733
+ */
734
+ buildMissingShallOrMustMessage(prefix, blockName, guidanceOnly = false) {
735
+ const base = `${prefix} ${guidanceOnly ? 'should' : 'must'} contain SHALL or MUST`;
736
+ const suffix = guidanceOnly ? ' (RFC 2119 best practice for English specs)' : '';
737
+ if (this.containsShallOrMust(blockName)) {
738
+ return `${base} in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.${suffix}`;
739
+ }
740
+ return `${base}${suffix}`;
741
+ }
742
+ countScenarios(blockRaw) {
743
+ // Fence-aware count via the shared reader: a `#### Scenario:` inside a fenced
744
+ // example is not a real scenario. Drop the header line (index 0).
745
+ return countScenariosShared(blockRaw.split('\n').slice(1));
746
+ }
747
+ formatSectionList(sections) {
748
+ if (sections.length === 0)
749
+ return '';
750
+ if (sections.length === 1)
751
+ return sections[0];
752
+ const head = sections.slice(0, -1);
753
+ const last = sections[sections.length - 1];
754
+ return `${head.join(', ')} and ${last}`;
755
+ }
756
+ }