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,654 @@
1
+ // Doctor checks that are independent of the install layout: runtime deps, settings.json
2
+ // hook conflicts, PROVENANCE baselines vs upstream, manifest drift, and the context-budget
3
+ // section (language lint + token budget, cache hygiene). Each returns
4
+ // printable lines (OK/WARN/INFO prefixed); index.ts composes them with the scope checks.
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { baselineStatus } from "../adapters/sync/diff.js";
9
+ import { PLUGIN_KEY } from "../adapters/memory/vendored.js";
10
+ import { manifestPath, readManifest } from "../infra/manifest.js";
11
+ import { retiredFindings } from "./cleanup.js";
12
+ import { languageLines, lintPaths } from "../adapters/content/language-lint.js";
13
+ import { isOurHook } from "./uninstall.js";
14
+ import { CLAUDE_MD_LINE_LIMIT, DEFAULT_PREFIX_BUDGET, cacheInvalidators, estimatePrefix, hookCount, lineCount, mcpRemoveCommand, mcpServerEntries, mcpServerNames, missingTtlKeys, readSettings } from "./prefix.js";
15
+ import { ANTHROPIC_BASE_URL_ENV, CLAUDE_CODE_SUBAGENT_MODEL_ENV, tiersFromEnv } from "@freeharness/core";
16
+ const DEPS = [
17
+ { name: "node", neededBy: "rules (memory/config guard, repeat-tool, session-start hooks), safety (security-guidance hook), cost (tracker, pruner, monitor hooks), skills (report-formatting docx)", version: ["node", "--version"] },
18
+ { name: "npm", neededBy: "skills (report-formatting docx deps), sdd (vendored openspec build)", version: ["npm", "--version"] },
19
+ { name: "bun", neededBy: "memory (worker build + plugin runtime)", version: ["bun", "--version"] },
20
+ { name: "uv", neededBy: "memory (Chroma vector store)", version: ["uv", "--version"] },
21
+ { name: "python3", neededBy: "skills (report-formatting pptx)", version: ["python3", "--version"] },
22
+ { name: "soffice", neededBy: "skills (report-formatting: pptx/docx -> pdf render)" },
23
+ { name: "pdftoppm", neededBy: "skills (report-formatting: pdf -> thumbnails)" },
24
+ { name: "python-pptx", neededBy: "skills (report-formatting pptx)", probe: ["python3", "-c", "import pptx"] },
25
+ ];
26
+ // Caller's env + PYTHONDONTWRITEBYTECODE=1, so a `python3` probe (e.g. `import pptx`) never
27
+ // leaves a bytecode cache under the home directory the probe runs against.
28
+ const PROBE_ENV = { ...process.env, PYTHONDONTWRITEBYTECODE: "1" };
29
+ /**
30
+ * Probes every runtime dependency on the current PATH.
31
+ * @returns One status per dependency, in table order.
32
+ */
33
+ export function probeDeps() {
34
+ return DEPS.map((d) => {
35
+ const probe = d.probe ? spawnSync(d.probe[0], d.probe.slice(1), { encoding: "utf8", env: PROBE_ENV }) : spawnSync("sh", ["-c", `command -v ${d.name}`], { encoding: "utf8", env: PROBE_ENV });
36
+ const present = probe.status === 0;
37
+ let version;
38
+ if (present && d.version) {
39
+ const v = spawnSync(d.version[0], d.version.slice(1), { encoding: "utf8", env: PROBE_ENV });
40
+ if (v.status === 0)
41
+ version = /\d+\.\d+[\w.-]*/.exec(v.stdout)?.[0] ?? v.stdout.trim().split("\n")[0];
42
+ }
43
+ return { name: d.name, present, version, neededBy: d.neededBy };
44
+ });
45
+ }
46
+ /**
47
+ * Renders the deps table.
48
+ * @param deps Probe results.
49
+ * @returns Printable lines.
50
+ */
51
+ export function depLines(deps) {
52
+ const lines = ["Runtime deps:"];
53
+ for (const d of deps) {
54
+ const name = d.name.padEnd(12);
55
+ lines.push(d.present ? ` OK ${name} ${(d.version ?? "present").padEnd(12)} needed by: ${d.neededBy}` : ` WARN ${name} ${"missing".padEnd(12)} needed by: ${d.neededBy}`);
56
+ }
57
+ return lines;
58
+ }
59
+ /** Hook conflicts in a settings.json: duplicate commands, a foreign memory-injecting SessionStart hook, claude-mem hooks kept while the plugin is installed; empty when settings.json is absent. */
60
+ export function hookConflictLines(settingsPath, pluginInstalled) {
61
+ let settings;
62
+ try {
63
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ const lines = [];
69
+ const hooks = settings.hooks ?? {};
70
+ let claudeMemHooks = 0;
71
+ for (const [event, entries] of Object.entries(hooks)) {
72
+ if (!Array.isArray(entries))
73
+ continue;
74
+ const seen = new Map();
75
+ for (const e of entries) {
76
+ for (const h of e.hooks ?? []) {
77
+ const cmd = h.command ?? "";
78
+ seen.set(cmd, (seen.get(cmd) ?? 0) + 1);
79
+ if (/claude-mem/.test(cmd))
80
+ claudeMemHooks++;
81
+ }
82
+ if (event === "SessionStart" && !isOurHook(e) && (e.hooks ?? []).some((h) => /memory|claude-mem|recall/i.test(h.command ?? ""))) {
83
+ lines.push(`WARN another SessionStart hook also injects memory: ${(e.hooks ?? []).map((h) => h.command).join(" ; ")} (context may be injected twice)`);
84
+ }
85
+ }
86
+ for (const [cmd, n] of seen)
87
+ if (n > 1)
88
+ lines.push(`WARN duplicate hook command in ${event} (${n}x): ${cmd}`);
89
+ }
90
+ if (claudeMemHooks > 0) {
91
+ lines.push(pluginInstalled
92
+ ? `WARN ${claudeMemHooks} claude-mem hook(s) in settings.json while the ${PLUGIN_KEY} plugin is installed — the plugin ships its own hooks.json; these duplicate it: run \`freeharness cleanup --scope user\``
93
+ : "OK claude-mem hook detected in settings.json (coexists; no plugin registered)");
94
+ }
95
+ return lines;
96
+ }
97
+ /** Canonical agent file names and skill dir names: what a project could duplicate, and what doctor tells apart from the user's own content. */
98
+ export function canonicalNames(canonicalDir) {
99
+ const list = (dir, dirsOnly) => {
100
+ if (!fs.existsSync(dir))
101
+ return [];
102
+ return fs
103
+ .readdirSync(dir, { withFileTypes: true })
104
+ .filter((e) => (dirsOnly ? e.isDirectory() : e.isFile() && e.name.endsWith(".md") && e.name !== "README.md"))
105
+ .map((e) => e.name);
106
+ };
107
+ return { agents: list(path.join(canonicalDir, "agents"), false), skills: list(path.join(canonicalDir, "skills"), true) };
108
+ }
109
+ /** Skills and agents under a Claude dir that FreeHarness did not emit (the user's own content); never a global `openspec-*` copy or a manifest-recorded retired name. */
110
+ export function foreignContentLines(claudeDir, canonicalDir, scope, projectRoot) {
111
+ const names = canonicalNames(canonicalDir);
112
+ const manifest = readManifest(manifestPath(scope, projectRoot));
113
+ const recordedRetired = new Set(retiredFindings(claudeDir, manifest).filter((f) => f.recorded).map((f) => f.path));
114
+ const lines = [];
115
+ const skillsDir = path.join(claudeDir, "skills");
116
+ if (fs.existsSync(skillsDir)) {
117
+ for (const e of fs.readdirSync(skillsDir, { withFileTypes: true })) {
118
+ if (!e.isDirectory() || e.name.startsWith(".") || e.name.startsWith("openspec-") || names.skills.includes(e.name))
119
+ continue;
120
+ if (recordedRetired.has(path.join(skillsDir, e.name)))
121
+ continue;
122
+ lines.push(`INFO skill ${e.name} not from FreeHarness (yours; kept)`);
123
+ }
124
+ }
125
+ const agentsDir = path.join(claudeDir, "agents");
126
+ if (fs.existsSync(agentsDir)) {
127
+ for (const e of fs.readdirSync(agentsDir, { withFileTypes: true })) {
128
+ if (!e.isFile() || !e.name.endsWith(".md") || names.agents.includes(e.name))
129
+ continue;
130
+ if (recordedRetired.has(path.join(agentsDir, e.name)))
131
+ continue;
132
+ lines.push(`INFO agent ${e.name.replace(/\.md$/, "")} not from FreeHarness (yours; kept)`);
133
+ }
134
+ }
135
+ return lines.sort();
136
+ }
137
+ /**
138
+ * Flags a CLI that runs from outside `~/.local/bin` while the install.sh symlink exists there:
139
+ * the shell resolved an older link (a global `npm link`, a stale PATH entry) ahead of ours.
140
+ * @param argv1 `process.argv[1]` of the running CLI.
141
+ * @param home Home directory.
142
+ * @returns One WARN line, or none.
143
+ */
144
+ export function runningFromLines(argv1, home) {
145
+ if (!argv1)
146
+ return [];
147
+ const localBin = path.join(home, ".local", "bin");
148
+ const ours = path.join(localBin, "freeharness");
149
+ const running = path.resolve(argv1);
150
+ if (running.startsWith(localBin + path.sep))
151
+ return [];
152
+ try {
153
+ fs.lstatSync(ours);
154
+ }
155
+ catch {
156
+ return [];
157
+ }
158
+ return [`WARN running from ${running}; ${ours} exists — open a new terminal or run \`freeharness cleanup --scope user\` (an old npm link may shadow it)`];
159
+ }
160
+ /**
161
+ * Whether claude-mem is enabled in settings.json or registered in installed_plugins.json.
162
+ * @param claudeDir The ~/.claude (or project .claude) dir.
163
+ * @returns True when found in either place.
164
+ */
165
+ export function memoryPluginInstalled(claudeDir) {
166
+ const read = (f) => {
167
+ try {
168
+ return JSON.parse(fs.readFileSync(f, "utf8"));
169
+ }
170
+ catch {
171
+ return {};
172
+ }
173
+ };
174
+ const settings = read(path.join(claudeDir, "settings.json"));
175
+ if (settings.enabledPlugins?.[PLUGIN_KEY] === true)
176
+ return true;
177
+ const ip = read(path.join(claudeDir, "plugins", "installed_plugins.json"));
178
+ return Boolean(ip.plugins && PLUGIN_KEY in ip.plugins);
179
+ }
180
+ /**
181
+ * PROVENANCE baseline vs upstream fork HEAD for each vendored engine.
182
+ * @param canonicalDir Resolved canonical directory.
183
+ * @returns Printable lines, one per engine.
184
+ */
185
+ export function baselineLines(canonicalDir) {
186
+ const lines = [];
187
+ for (const target of ["sdd", "memory"]) {
188
+ const s = baselineStatus(canonicalDir, target);
189
+ if (!s.forkFound) {
190
+ lines.push(`INFO ${target}: PROVENANCE baseline ${s.baseline}; fork not found at ${s.fork} (set FREEHARNESS_UPSTREAM_${target.toUpperCase()})`);
191
+ }
192
+ else if (!s.head) {
193
+ lines.push(`INFO ${target}: PROVENANCE baseline ${s.baseline}; fork at ${s.fork} is not a git checkout`);
194
+ }
195
+ else if (s.head === s.baseline || s.baseline.startsWith(s.head) || s.head.startsWith(s.baseline)) {
196
+ lines.push(`OK ${target}: PROVENANCE baseline ${s.baseline} == fork HEAD`);
197
+ }
198
+ else {
199
+ lines.push(`WARN ${target}: PROVENANCE baseline ${s.baseline} != fork HEAD ${s.head} (run: freeharness sync check ${target})`);
200
+ }
201
+ }
202
+ return lines;
203
+ }
204
+ /** Complete-but-not-archived ids named in the summary, at most. */
205
+ const MAX_COMPLETE_IDS = 5;
206
+ /**
207
+ * Reads the change status from tasks.md checkbox counts and the optional blockers file.
208
+ * @param projectRoot Project root.
209
+ * @returns Status, or null when <projectRoot>/openspec/changes is absent.
210
+ */
211
+ export function openspecChangeStatus(projectRoot) {
212
+ const dir = path.join(projectRoot, "openspec", "changes");
213
+ let entries;
214
+ try {
215
+ entries = fs.readdirSync(dir, { withFileTypes: true });
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ const inFlight = [];
221
+ const complete = [];
222
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
223
+ if (!e.isDirectory() || e.name === "archive" || e.name.startsWith("."))
224
+ continue;
225
+ inFlight.push(e.name);
226
+ let text;
227
+ try {
228
+ text = fs.readFileSync(path.join(dir, e.name, "tasks.md"), "utf8");
229
+ }
230
+ catch {
231
+ continue;
232
+ }
233
+ const open = (text.match(/^\s*- \[ \]/gm) ?? []).length;
234
+ const done = (text.match(/^\s*- \[[xX]\]/gm) ?? []).length;
235
+ if (open === 0 && done > 0)
236
+ complete.push(e.name);
237
+ }
238
+ let blocked = null;
239
+ try {
240
+ const parsed = JSON.parse(fs.readFileSync(path.join(projectRoot, ".freeharness", "sdd", "blockers.json"), "utf8"));
241
+ const list = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.blocked) ? parsed.blocked : null;
242
+ if (list)
243
+ blocked = list.filter((id) => typeof id === "string" && inFlight.includes(id));
244
+ }
245
+ catch {
246
+ blocked = null;
247
+ }
248
+ return { inFlight, complete, blocked };
249
+ }
250
+ /**
251
+ * The SDD status summary: WARN naming the complete-but-not-archived changes, OK otherwise.
252
+ * @param projectRoot Project root.
253
+ * @returns One line, or none when the project has no openspec/changes.
254
+ */
255
+ export function openspecChangeLines(projectRoot) {
256
+ const s = openspecChangeStatus(projectRoot);
257
+ if (!s)
258
+ return [];
259
+ const blocked = s.blocked === null ? "" : `, ${s.blocked.length} blocked by unresolved headers`;
260
+ const tail = `${s.inFlight.length} change(s) in flight${blocked}; rule: finish or archive before opening a new change (/opsx:archive)`;
261
+ if (s.complete.length === 0)
262
+ return [`OK openspec: ${tail}`];
263
+ const shown = s.complete.slice(0, MAX_COMPLETE_IDS);
264
+ const ids = `${shown.join(", ")}${s.complete.length > shown.length ? ", ..." : ""}`;
265
+ return [`WARN complete changes not archived: ${ids} — ${s.complete.length} of ${tail}`];
266
+ }
267
+ /**
268
+ * Install-manifest presence and drift (listed files that are gone).
269
+ * @param manifestFile The manifest path for the scope.
270
+ * @returns Printable lines.
271
+ */
272
+ export function manifestLines(manifestFile) {
273
+ const m = readManifest(manifestFile);
274
+ if (!m)
275
+ return [`WARN install manifest not found at ${manifestFile} (run init; uninstall/update --check need it)`];
276
+ const missing = m.files.filter((f) => !fs.existsSync(f.path)).length;
277
+ const lines = [`OK install manifest: ${m.files.length} files, v${m.version}${m.sourceCommit ? ` @${m.sourceCommit}` : ""}, installed ${m.installedAt}`];
278
+ if (missing > 0)
279
+ lines.push(`WARN drift: ${missing} files listed in the manifest are gone — freeharness cleanup drops removed copies from the manifest; freeharness update re-emits what should exist`);
280
+ else
281
+ lines.push("OK no drift: every manifest file is present");
282
+ return lines;
283
+ }
284
+ /**
285
+ * Roots of the emitted prose for a scope: the rules file plus the harness content dirs.
286
+ * @param scope `user` or `project`.
287
+ * @param base `~/.claude` (user) or the project root (project).
288
+ * @returns Existing or not, in scan order; missing roots are skipped by the lint.
289
+ */
290
+ export function emittedProseRoots(scope, base) {
291
+ if (scope === "user")
292
+ return [path.join(base, "CLAUDE.md"), ...["skills", "agents", "commands", "freeharness"].map((d) => path.join(base, d))];
293
+ return [path.join(base, "CLAUDE.md"), path.join(base, ".claude")];
294
+ }
295
+ /**
296
+ * `doctor --context-budget` section: the language lint over canonical and the emitted
297
+ * content (gerund preambles, Portuguese outside declared pt-BR sections). Advisory only.
298
+ * @param canonicalDir Resolved canonical directory (null when unresolved).
299
+ * @param emittedRoots Emitted prose roots for the scope.
300
+ * @returns Printable lines, indented under a `Context budget:` header.
301
+ */
302
+ export function contextBudgetLines(canonicalDir, emittedRoots) {
303
+ const lines = ["Context budget:"];
304
+ const roots = [...(canonicalDir ? [canonicalDir] : []), ...emittedRoots];
305
+ for (const l of languageLines(lintPaths(roots)))
306
+ lines.push(` ${l}`);
307
+ return lines;
308
+ }
309
+ /**
310
+ * Token side of `doctor --context-budget`: CLAUDE.md lines vs 200, hook/MCP counts, the
311
+ * prefix estimate vs budget, cache TTLs and cache invalidators. Each line is OK or FAIL; the
312
+ * section never blocks.
313
+ * @param inp Home, project root, budget.
314
+ * @returns Printable lines (indented, no header).
315
+ */
316
+ export function budgetLines(inp) {
317
+ const home = inp.home;
318
+ const claudeDir = path.join(home, ".claude");
319
+ const globalRules = path.join(claudeDir, "CLAUDE.md");
320
+ const projectRules = path.join(inp.projectRoot, "CLAUDE.md");
321
+ const userSettingsFile = path.join(claudeDir, "settings.json");
322
+ const projectSettingsFiles = [path.join(inp.projectRoot, ".claude", "settings.json"), path.join(inp.projectRoot, ".claude", "settings.local.json")];
323
+ const lines = [];
324
+ const tag = (ok) => (ok ? " OK " : " FAIL");
325
+ const pl = lineCount(projectRules);
326
+ lines.push(fs.existsSync(projectRules) ? `${tag(pl <= CLAUDE_MD_LINE_LIMIT)} CLAUDE.md (project): ${pl} lines (limit ${CLAUDE_MD_LINE_LIMIT}) — ${projectRules}` : ` OK CLAUDE.md (project): none at ${projectRules}`);
327
+ const gl = lineCount(globalRules);
328
+ lines.push(fs.existsSync(globalRules) ? `${tag(gl <= CLAUDE_MD_LINE_LIMIT)} global rules: ${gl} lines (limit ${CLAUDE_MD_LINE_LIMIT}) — ${globalRules}` : ` FAIL global rules: missing ${globalRules} (run init --scope user)`);
329
+ const hooks = hookCount(userSettingsFile) + projectSettingsFiles.reduce((n, f) => n + hookCount(f), 0);
330
+ const mcp = mcpServerNames(home, inp.projectRoot);
331
+ lines.push(` OK hooks: ${hooks} command(s) registered (user + project settings); MCP servers: ${mcp.length}${mcp.length > 0 ? ` (${mcp.join(", ")})` : ""}`);
332
+ const est = estimatePrefix(claudeDir, [globalRules, projectRules], mcp, inp.budget ?? DEFAULT_PREFIX_BUDGET);
333
+ const top = est.items.slice(0, 3).map((i) => `${path.basename(path.dirname(i.source)) === "skills" || i.kind === "skill description" ? path.basename(path.dirname(i.source)) : path.basename(i.source)} ${i.tokens}`).join(", ");
334
+ lines.push(`${tag(est.ok)} prefix estimate: ~${est.tokens} tokens over ${est.items.length} sources (budget ${est.budget}; words x 1.3)${top ? `; top: ${top}` : ""}`);
335
+ const userSettings = readSettings(userSettingsFile);
336
+ const missing = missingTtlKeys(userSettings);
337
+ lines.push(missing.length === 0 ? ` OK cache TTL: promptCacheTtl=${String(userSettings.promptCacheTtl)}, subagentPromptCacheTtl=${String(userSettings.subagentPromptCacheTtl)}` : ` FAIL cache TTL: ${missing.join(", ")} missing in ${userSettingsFile} (freeharness init --scope user --only cost)`);
338
+ const projectSettings = Object.assign({}, ...projectSettingsFiles.map(readSettings));
339
+ const inv = cacheInvalidators(userSettings, projectSettings);
340
+ lines.push(inv.length === 0
341
+ ? " OK cache invalidators: none (project settings do not override model / effortLevel / permissions.deny)"
342
+ : ` FAIL cache invalidators: ${inv.map((i) => `${i.key}=${JSON.stringify(i.project)} (user: ${i.user === undefined ? "unset" : JSON.stringify(i.user)})`).join("; ")} — project overrides in ${projectSettingsFiles[0]} invalidate the prompt cache; set them at user scope`);
343
+ lines.push(contextWindowLine(userSettings, projectSettings));
344
+ return lines;
345
+ }
346
+ /**
347
+ * Splits a model id on a trailing `[suffix]` tag (e.g. `claude-fable-5-1[1m]`).
348
+ * @param model Model id as recorded in settings.json.
349
+ * @returns Base id and suffix (null when the id carries none).
350
+ */
351
+ function splitModelSuffix(model) {
352
+ const m = /^(.*)\[([^[\]]+)\]$/.exec(model);
353
+ return m ? { base: m[1], suffix: m[2] } : { base: model, suffix: null };
354
+ }
355
+ /**
356
+ * Context-window line of `doctor --context-budget`: WARN when the user or project `model`
357
+ * carries a `[1m]` suffix (a week on it measured 1.69B cache-read tokens), naming the suffix
358
+ * and the standard-window id; OK otherwise, naming the effective model (project overrides user).
359
+ * @param userSettings `~/.claude/settings.json` content.
360
+ * @param projectSettings Merged `<project>/.claude/settings.json` + `settings.local.json`.
361
+ * @returns One printable line.
362
+ */
363
+ export function contextWindowLine(userSettings, projectSettings) {
364
+ const userModel = typeof userSettings.model === "string" ? userSettings.model : undefined;
365
+ const projectModel = typeof projectSettings.model === "string" ? projectSettings.model : undefined;
366
+ const userSplit = userModel ? splitModelSuffix(userModel) : undefined;
367
+ const projectSplit = projectModel ? splitModelSuffix(projectModel) : undefined;
368
+ const longContext = userSplit?.suffix?.toLowerCase() === "1m" ? { scope: "user", ...userSplit } : projectSplit?.suffix?.toLowerCase() === "1m" ? { scope: "project", ...projectSplit } : undefined;
369
+ if (longContext) {
370
+ return ` WARN context window: ${longContext.scope} settings.json model ${longContext.base}[${longContext.suffix}] (1M tokens); a week on [${longContext.suffix}] cost 1.69G cache-read tokens; standard window: ${longContext.base}`;
371
+ }
372
+ const effective = projectModel ?? userModel;
373
+ return ` OK context window: standard (${effective ?? "not set"})`;
374
+ }
375
+ /** ms; a local server that misses this is down. */
376
+ export const PROBE_TIMEOUT_MS = 1500;
377
+ /**
378
+ * GETs a JSON endpoint with a timeout.
379
+ * @param url URL.
380
+ * @param timeoutMs Timeout.
381
+ * @returns Parsed JSON, or the HTTP status as an error.
382
+ */
383
+ async function getJson(url, timeoutMs) {
384
+ const ctl = new AbortController();
385
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
386
+ try {
387
+ const r = await fetch(url, { signal: ctl.signal });
388
+ let body;
389
+ try {
390
+ body = await r.json();
391
+ }
392
+ catch {
393
+ body = undefined;
394
+ }
395
+ return { status: r.status, body };
396
+ }
397
+ finally {
398
+ clearTimeout(timer);
399
+ }
400
+ }
401
+ /**
402
+ * Probes a local model server: `<base>/api/version` (Ollama) first, then `<base>/v1/models`
403
+ * (any OpenAI-compatible server such as llama.cpp, vLLM, LM Studio). Down = error text
404
+ * (ECONNREFUSED, timeout), never a throw.
405
+ * @param baseUrl The ANTHROPIC_BASE_URL / provider base URL.
406
+ * @param timeoutMs Timeout per request.
407
+ * @returns Probe result.
408
+ */
409
+ export async function probeLocalServer(baseUrl, timeoutMs = PROBE_TIMEOUT_MS) {
410
+ const base = baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
411
+ const errors = [];
412
+ try {
413
+ const v = await getJson(`${base}/api/version`, timeoutMs);
414
+ const version = v.body?.version;
415
+ if (v.status === 200 && typeof version === "string") {
416
+ let models;
417
+ try {
418
+ const tags = await getJson(`${base}/api/tags`, timeoutMs);
419
+ const list = tags.body?.models;
420
+ if (Array.isArray(list))
421
+ models = list.map((m) => m.name ?? "").filter(Boolean);
422
+ }
423
+ catch {
424
+ models = undefined;
425
+ }
426
+ return { ok: true, kind: "ollama", detail: `ollama ${version}`, models };
427
+ }
428
+ errors.push(`/api/version -> HTTP ${v.status}`);
429
+ }
430
+ catch (e) {
431
+ errors.push(`/api/version -> ${describeFetchError(e, timeoutMs)}`);
432
+ }
433
+ try {
434
+ const m = await getJson(`${base}/v1/models`, timeoutMs);
435
+ const data = m.body?.data;
436
+ if (m.status === 200 && Array.isArray(data)) {
437
+ const models = data.map((d) => d.id ?? "").filter(Boolean);
438
+ return { ok: true, kind: "openai", detail: `openai-compatible, ${models.length} model(s)`, models };
439
+ }
440
+ errors.push(`/v1/models -> HTTP ${m.status}`);
441
+ }
442
+ catch (e) {
443
+ errors.push(`/v1/models -> ${describeFetchError(e, timeoutMs)}`);
444
+ }
445
+ return { ok: false, error: errors.join("; ") };
446
+ }
447
+ /**
448
+ * Short text for a fetch failure (the cause's code when present: ECONNREFUSED, ENOTFOUND).
449
+ * @param e Thrown value.
450
+ * @param timeoutMs Timeout used, for the abort message.
451
+ * @returns Text.
452
+ */
453
+ function describeFetchError(e, timeoutMs) {
454
+ if (e instanceof Error) {
455
+ if (e.name === "AbortError")
456
+ return `timeout after ${timeoutMs} ms`;
457
+ const cause = e.cause;
458
+ return cause?.code ?? cause?.message ?? e.message;
459
+ }
460
+ return String(e);
461
+ }
462
+ /**
463
+ * The active local-model profile as seen in a settings.json env block.
464
+ * @param settingsFile settings.json path.
465
+ * @returns Base URL and tier models, or undefined when no base URL is set.
466
+ */
467
+ export function activeProfile(settingsFile) {
468
+ const env = (readSettings(settingsFile).env ?? {});
469
+ const baseUrl = env[ANTHROPIC_BASE_URL_ENV];
470
+ if (typeof baseUrl !== "string" || !baseUrl)
471
+ return undefined;
472
+ const sub = env[CLAUDE_CODE_SUBAGENT_MODEL_ENV];
473
+ return { baseUrl, tiers: tiersFromEnv(env), subagent: typeof sub === "string" ? sub : undefined };
474
+ }
475
+ /**
476
+ * Doctor's profile line: no base URL = one OK line (default endpoint); a base URL = the
477
+ * probe result with the server version and the tier models (FAIL with the error when down,
478
+ * WARN for a tier model the server does not list).
479
+ * @param settingsFile settings.json path.
480
+ * @param probe Probe function (injectable for tests).
481
+ * @returns Printable lines.
482
+ */
483
+ export async function profileLines(settingsFile, probe = probeLocalServer) {
484
+ const p = activeProfile(settingsFile);
485
+ if (!p)
486
+ return ["OK profile: none (default Anthropic endpoint; `freeharness init --profile local-ollama` to use a local server)"];
487
+ const tiers = ["strong", "standard", "fast"].map((t) => `${t}=${p.tiers[t] ?? "(unset)"}`).join(" ");
488
+ const r = await probe(p.baseUrl);
489
+ if (!r.ok)
490
+ return [`FAIL profile: ${p.baseUrl} down — ${r.error} (start the server, or \`freeharness init --profile none\` to return to the default endpoint); tiers ${tiers}`];
491
+ const lines = [`OK profile: ${p.baseUrl} up (${r.detail}); tiers ${tiers}${p.subagent ? `; subagents=${p.subagent}` : ""}`];
492
+ if (r.models && r.models.length > 0) {
493
+ for (const t of ["strong", "standard", "fast"]) {
494
+ const m = p.tiers[t];
495
+ if (m && !r.models.includes(m) && !r.models.includes(`${m}:latest`))
496
+ lines.push(`WARN profile: ${t} model ${m} is not listed by the server (${r.models.slice(0, 6).join(", ")}${r.models.length > 6 ? ", ..." : ""})`);
497
+ }
498
+ }
499
+ return lines;
500
+ }
501
+ /**
502
+ * Tool counts per MCP server name from the catalogs (the mcp profile and servers.json).
503
+ * @param canonicalDir Resolved canonical directory.
504
+ * @returns name -> approximate tool count.
505
+ */
506
+ export function mcpToolCounts(canonicalDir) {
507
+ const counts = {};
508
+ try {
509
+ const cat = JSON.parse(fs.readFileSync(path.join(canonicalDir, "profiles", "mcp.json"), "utf8"));
510
+ for (const s of cat.servers)
511
+ counts[s.name] = s.tools_approx;
512
+ }
513
+ catch {
514
+ // no catalog bundled
515
+ }
516
+ try {
517
+ const base = JSON.parse(fs.readFileSync(path.join(canonicalDir, "mcp", "servers.json"), "utf8"));
518
+ for (const s of base.servers)
519
+ if (typeof s.tools_approx === "number" && !(s.name in counts))
520
+ counts[s.name] = s.tools_approx;
521
+ }
522
+ catch {
523
+ // no servers.json
524
+ }
525
+ return counts;
526
+ }
527
+ /**
528
+ * MCP servers with their catalog tool counts, then one line per (server, scope) naming where
529
+ * it lives and the exact `claude mcp remove -s <scope>` command (Claude Code keeps user/local
530
+ * scope in `.claude.json`, project scope in `<project>/.mcp.json`).
531
+ * @param home Home directory.
532
+ * @param projectRoot Project root.
533
+ * @param counts Tool counts from `mcpToolCounts`.
534
+ * @returns Printable lines.
535
+ */
536
+ export function mcpLines(home, projectRoot, counts) {
537
+ const entries = mcpServerEntries(home, projectRoot);
538
+ if (entries.length === 0)
539
+ return ["OK mcp: no servers registered (~/.claude.json user + local scopes, <project>/.mcp.json project scope)"];
540
+ const names = [...new Set(entries.map((e) => e.name))];
541
+ const total = names.reduce((n, s) => n + (counts[s] ?? 0), 0);
542
+ const scopesOf = (n) => [...new Set(entries.filter((e) => e.name === n).map((e) => e.scope))].join("+");
543
+ const lines = [`OK mcp: ${names.length} server(s), ~${total} tools of schemas on every request: ${names.map((n) => `${n} (${counts[n] !== undefined ? `~${counts[n]} tools` : "no tool count recorded"}, ${scopesOf(n)})`).join(", ")}`];
544
+ for (const e of entries) {
545
+ const where = e.scope === "user"
546
+ ? `user scope in ${e.file}`
547
+ : e.scope === "local"
548
+ ? `local scope for ${e.projectPath}${e.projectPath === projectRoot ? " (this project)" : ""} in ${e.file}`
549
+ : `project scope in ${e.file}`;
550
+ const catalog = counts[e.name] === undefined ? "; not in the FreeHarness catalog (context cost unknown)" : "";
551
+ lines.push(`INFO mcp: ${e.name} — ${where}${catalog}; remove: ${mcpRemoveCommand(e)}`);
552
+ }
553
+ return lines;
554
+ }
555
+ /**
556
+ * What the install dirs hold: skill directories and agent `.md` files. Dotfiles (`.DS_Store`
557
+ * on macOS) and stray files never count.
558
+ * @param claudeDir `~/.claude` (or a project `.claude`).
559
+ * @returns Counts, or -1 for a missing directory.
560
+ */
561
+ export function installedContentCounts(claudeDir) {
562
+ const count = (dir, keep) => {
563
+ if (!fs.existsSync(dir))
564
+ return -1;
565
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && keep(e)).length;
566
+ };
567
+ return {
568
+ skills: count(path.join(claudeDir, "skills"), (e) => e.isDirectory()),
569
+ agents: count(path.join(claudeDir, "agents"), (e) => e.isFile() && e.name.endsWith(".md")),
570
+ };
571
+ }
572
+ /** Roots a hook command may point at without being flagged foreign. */
573
+ export function trustedHookRoots(home) {
574
+ return [path.join(home, ".claude", "freeharness"), path.join(home, ".claude", "plugins"), path.join(home, ".claude-mem"), "${CLAUDE_PLUGIN_ROOT}", "${CLAUDE_PROJECT_DIR}"];
575
+ }
576
+ /**
577
+ * Extracts the script path of a hook command: the first quoted or bare token that looks
578
+ * like a path (starts with `/`, `~` or `$`), skipping an interpreter (`node`, `bash`, `sh`,
579
+ * `bun`, `python3`).
580
+ * @param command Hook command.
581
+ * @returns The path and whether an interpreter precedes it; undefined for a bare command.
582
+ */
583
+ export function hookScriptPath(command) {
584
+ const tokens = [...command.matchAll(/"([^"]+)"|'([^']+)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3]);
585
+ if (tokens.length === 0)
586
+ return undefined;
587
+ const interpreters = new Set(["node", "bash", "sh", "bun", "python3", "python", "npx", "uv", "uvx"]);
588
+ let interpreter;
589
+ for (const t of tokens) {
590
+ if (interpreters.has(t) && interpreter === undefined) {
591
+ interpreter = t;
592
+ continue;
593
+ }
594
+ if (t.startsWith("-"))
595
+ continue;
596
+ if (/^[/~$]/.test(t))
597
+ return { script: t, interpreter };
598
+ return undefined;
599
+ }
600
+ return undefined;
601
+ }
602
+ /**
603
+ * Scans the hook commands of a settings.json: a command whose script lies outside the
604
+ * trusted roots is WARN foreign; a script that is missing, or invoked directly without the
605
+ * executable bit, is WARN.
606
+ * @param settingsPath settings.json path.
607
+ * @param home Home directory.
608
+ * @returns Printable lines (an OK line when nothing is found; empty when the file is absent).
609
+ */
610
+ export function hookScanLines(settingsPath, home) {
611
+ const settings = readSettings(settingsPath);
612
+ if (!settings.hooks)
613
+ return [];
614
+ const roots = trustedHookRoots(home);
615
+ const lines = [];
616
+ let scanned = 0;
617
+ for (const [event, entries] of Object.entries(settings.hooks)) {
618
+ for (const e of entries ?? []) {
619
+ for (const h of e.hooks ?? []) {
620
+ const cmd = h.command ?? "";
621
+ if (!cmd)
622
+ continue;
623
+ scanned++;
624
+ const sp = hookScriptPath(cmd);
625
+ if (!sp) {
626
+ lines.push(`WARN hook scan: ${event} runs a bare command outside the FreeHarness, plugin and memory directories: ${cmd}`);
627
+ continue;
628
+ }
629
+ const expanded = sp.script.startsWith("~") ? path.join(home, sp.script.slice(1)) : sp.script;
630
+ if (!roots.some((r) => expanded === r || expanded.startsWith(r.endsWith("/") ? r : `${r}/`) || expanded.startsWith(`${r}${path.sep}`))) {
631
+ lines.push(`WARN hook scan: ${event} command points outside the FreeHarness, plugin and memory directories (foreign): ${expanded}`);
632
+ continue;
633
+ }
634
+ if (expanded.startsWith("$"))
635
+ continue;
636
+ if (!fs.existsSync(expanded)) {
637
+ lines.push(`WARN hook scan: ${event} script missing: ${expanded}`);
638
+ continue;
639
+ }
640
+ if (!sp.interpreter) {
641
+ try {
642
+ fs.accessSync(expanded, fs.constants.X_OK);
643
+ }
644
+ catch {
645
+ lines.push(`WARN hook scan: ${event} script is not executable (chmod +x): ${expanded}`);
646
+ }
647
+ }
648
+ }
649
+ }
650
+ }
651
+ if (lines.length === 0)
652
+ lines.push(`OK hook scan: ${scanned} command(s) inside the FreeHarness, plugin and memory directories; scripts present and executable`);
653
+ return lines;
654
+ }
@@ -0,0 +1,5 @@
1
+ import type { AdapterResult, ExecStep } from "@freeharness/core";
2
+ import type { PackagePort } from "@freeharness/core";
3
+ export declare function collectActions(results: AdapterResult[]): ExecStep[];
4
+ export declare function printPlan(results: AdapterResult[], actions: ExecStep[], dryRun: boolean): void;
5
+ export declare function runActions(actions: ExecStep[], pkg: PackagePort): Promise<void>;