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
package/dist/index.js ADDED
@@ -0,0 +1,989 @@
1
+ #!/usr/bin/env node
2
+ // FreeHarness CLI (driving adapter): init, update, bootstrap, cleanup, doctor, list, cost, memory, sync, uninstall.
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { Command } from "commander";
8
+ import { ALL_TARGETS, CAPABILITIES, DEFAULT_TARGETS, defaultCapabilityIds } from "@freeharness/core";
9
+ import { buildPlan } from "@freeharness/core";
10
+ import { cliVersion, resolveCanonicalDir, sourceRepoRoot } from "./infra/paths.js";
11
+ import { manifestPath, readManifest, repoHead, writeManifest } from "./infra/manifest.js";
12
+ import { runPlan } from "./cli/runner.js";
13
+ import { collectActions, printPlan, runActions } from "./cli/executor.js";
14
+ import { uninstallScope } from "./cli/uninstall.js";
15
+ import { applyCleanup, describeItem, planCleanup, pruneManifest } from "./cli/cleanup.js";
16
+ import { FH_CONTEXT_COMMAND } from "./adapters/harness/claude-code.js";
17
+ import { baselineLines, budgetLines, contextBudgetLines, depLines, emittedProseRoots, foreignContentLines, hookConflictLines, hookScanLines, installedContentCounts, manifestLines, mcpLines, mcpToolCounts, memoryPluginInstalled, openspecChangeLines, probeDeps, profileLines, runningFromLines } from "./cli/doctor.js";
18
+ import { applyEnvProfile, applyMcpProfile, applyProviderProfile, loadProfileFile, mcpCatalogItems, removeProfiles, resolveProfile } from "./cli/profile.js";
19
+ import { formatValidation, validatePluginDir } from "./cli/plugin-validate.js";
20
+ import { PROFILES, PROFILE_NONE } from "@freeharness/core";
21
+ import { agents, aggregate, changes, costLogLine, costLogPath, costPricesPath, formatAgents, formatChanges, formatInjectionLine, formatReport, formatSinks, loadPriceTable, readTelemetry, readUsage, sinks, telemetryPath } from "./cli/cost.js";
22
+ import { DEFAULT_PREFIX_BUDGET, estimatePrefix, mcpServerNames } from "./cli/prefix.js";
23
+ import { HOOK_SCRIPTS } from "./adapters/harness/claude-code.js";
24
+ import { configMarkerPath, openConfigWindow } from "./infra/fsutil.js";
25
+ import { NodePackage } from "./adapters/package/node.js";
26
+ import { ReadlinePrompt } from "./adapters/prompt/readline.js";
27
+ import { DiffSync } from "./adapters/sync/diff.js";
28
+ import { activateOurs, applyObserverSettings, backupMemoryData, dataDir, diagnoseMemory, previewObserverSettings, restartWorker } from "./adapters/memory/doctor.js";
29
+ const program = new Command();
30
+ program.name("freeharness").description("Agnostic agentic engineering framework installer").version(cliVersion());
31
+ function parseTargets(value) {
32
+ const items = value.split(",").map((s) => s.trim()).filter(Boolean);
33
+ const valid = new Set(ALL_TARGETS);
34
+ const bad = items.filter((i) => !valid.has(i));
35
+ if (bad.length > 0) {
36
+ throw new Error(`Unknown target(s): ${bad.join(", ")}. Valid: ${ALL_TARGETS.join(", ")}`);
37
+ }
38
+ return items;
39
+ }
40
+ function parseScope(value) {
41
+ if (value !== "project" && value !== "user") {
42
+ throw new Error(`Unknown scope: ${value}. Valid: project, user`);
43
+ }
44
+ return value;
45
+ }
46
+ /**
47
+ * Profile pass (v4 P6): `init --profile <id>` applies one profile file to the scope, never the
48
+ * capability plan. Same two passes as the install: preview (writes nothing), then confirm
49
+ * (or --yes) and apply; the manifest records the keys and files written so `--profile none`
50
+ * removes exactly them. Profiles are never part of the default set.
51
+ * @param opts CLI options.
52
+ */
53
+ async function applyProfile(opts) {
54
+ const id = opts.profile;
55
+ const scope = parseScope(opts.scope);
56
+ const targets = parseTargets(opts.target);
57
+ const canonicalDir = resolveCanonicalDir();
58
+ const projectRoot = path.resolve(opts.project);
59
+ const claudeDir = scope === "user" ? path.join(os.homedir(), ".claude") : path.join(projectRoot, ".claude");
60
+ const existing = readManifest(manifestPath(scope, projectRoot))?.profiles ?? {};
61
+ const profile = resolveProfile(id);
62
+ console.log(`Profile: ${id}${profile ? ` — ${profile.description}` : " (remove every recorded profile)"}`);
63
+ console.log(`Scope: ${scope}${scope === "user" ? ` (${claudeDir})` : ` (${projectRoot})`}`);
64
+ console.log(`Canonical: ${canonicalDir}`);
65
+ const build = (dryRun, selected) => {
66
+ const ctx = { canonicalDir, scope, projectRoot, claudeDir, dryRun, existing };
67
+ if (!profile)
68
+ return removeProfiles(existing, dryRun);
69
+ // A profile targets one harness; --target narrows nothing here but must not contradict it.
70
+ const target = profile.targets.find((t) => targets.includes(t)) ?? (targets.includes("plugin") ? undefined : profile.targets[0]);
71
+ if (!target)
72
+ throw new Error(`profile ${id} targets ${profile.targets.join(", ")}; pass --target ${profile.targets[0]}`);
73
+ if (target === "opencode" && scope !== "project")
74
+ throw new Error(`profile ${id} writes <project>/opencode.json: use --scope project --project <dir>`);
75
+ const data = loadProfileFile(canonicalDir, profile);
76
+ switch (profile.kind) {
77
+ case "claude-code-env":
78
+ return [applyEnvProfile(profile, data, ctx)];
79
+ case "opencode-provider":
80
+ return [applyProviderProfile(profile, data, ctx)];
81
+ case "mcp-catalog":
82
+ return [applyMcpProfile(profile, data, selected, ctx)];
83
+ default:
84
+ throw new Error(`profile ${id}: unknown kind`);
85
+ }
86
+ };
87
+ const print = (plans, tag) => {
88
+ for (const p of plans) {
89
+ console.log(`\n${tag}${p.target} :: profile ${p.profileId}`);
90
+ for (const l of p.lines)
91
+ console.log(l);
92
+ if (p.actions.length > 0) {
93
+ console.log(`\n${tag}actions:`);
94
+ for (const a of p.actions)
95
+ console.log(` - ${a.description}${a.optional ? " (optional)" : ""}\n $ ${a.command}`);
96
+ }
97
+ }
98
+ if (plans.length === 0)
99
+ console.log(`\n${tag}no profile recorded in the manifest; nothing to remove.`);
100
+ };
101
+ // Selection (mcp profile): --select for non-interactive runs, else the prompt; no TTY = none.
102
+ let selected = [];
103
+ if (profile?.kind === "mcp-catalog") {
104
+ const items = mcpCatalogItems(loadProfileFile(canonicalDir, profile));
105
+ // --select takes names (unknown ones are reported by the plan); the prompt takes numbers too.
106
+ if (opts.select !== undefined)
107
+ selected = opts.select.split(",").map((t) => t.trim()).filter(Boolean);
108
+ else if (!opts.dryRun)
109
+ selected = await new ReadlinePrompt().select("\nMCP catalog (nothing is registered without a selection):", items);
110
+ if (opts.dryRun && opts.select === undefined) {
111
+ console.log("\nMCP catalog (dry-run shows the catalog; pass --select a,b to preview a selection):");
112
+ for (const it of items)
113
+ console.log(` - ${it.id.padEnd(14)} ${it.label}`);
114
+ }
115
+ }
116
+ const preview = build(true, selected);
117
+ print(preview, opts.dryRun ? "[dry-run] " : "");
118
+ if (opts.dryRun) {
119
+ console.log("\n[dry-run] nothing was applied.");
120
+ return;
121
+ }
122
+ const changes = preview.some((p) => p.lines.some((l) => /^ (edit|write|rm|mcp):/.test(l)) || p.actions.length > 0);
123
+ if (!changes) {
124
+ console.log("\nNothing to apply.");
125
+ return;
126
+ }
127
+ if (!opts.yes && !(await new ReadlinePrompt().confirm("\nApply these changes?"))) {
128
+ console.log("Aborted; nothing was applied.");
129
+ return;
130
+ }
131
+ const closeWindow = openConfigWindow(configMarkerPath(scope, projectRoot, os.homedir()));
132
+ let plans;
133
+ try {
134
+ plans = build(false, selected);
135
+ }
136
+ finally {
137
+ closeWindow();
138
+ }
139
+ const records = {};
140
+ for (const p of plans)
141
+ if (p.record !== undefined)
142
+ records[p.profileId] = p.record;
143
+ const repo = sourceRepoRoot(canonicalDir);
144
+ const manifest = writeManifest({
145
+ version: cliVersion(),
146
+ sourceCommit: repo ? repoHead(repo) : undefined,
147
+ scope,
148
+ projectRoot,
149
+ targets: [...new Set(plans.map((p) => p.target))],
150
+ capabilities: [],
151
+ results: plans.map((p) => ({ capabilityId: `profile:${p.profileId}`, target: p.target, written: p.written, actions: p.actions, notes: [] })),
152
+ actions: plans.flatMap((p) => p.actions.map((a) => a.description)),
153
+ profiles: records,
154
+ });
155
+ console.log(`\nmanifest: ${manifest}`);
156
+ const actions = collectActions(plans.map((p) => ({ capabilityId: p.profileId, target: p.target, written: [], actions: p.actions, notes: [] })));
157
+ if (actions.length > 0)
158
+ await runActions(actions, new NodePackage());
159
+ console.log("\nApplied.");
160
+ }
161
+ // Two-pass install: pass 1 previews (writes nothing), then (unless --dry-run) confirm
162
+ // and pass 2 applies file writes, then run the side-effecting actions.
163
+ async function applyPlan(opts) {
164
+ const targets = parseTargets(opts.target);
165
+ const scope = parseScope(opts.scope);
166
+ if (targets.includes("plugin") && scope !== "project") {
167
+ throw new Error("--target plugin writes a plugin directory under --project <dir>; it is project-scoped (use --scope project).");
168
+ }
169
+ const requested = opts.only
170
+ ? opts.only.split(",").map((s) => s.trim()).filter(Boolean)
171
+ : defaultCapabilityIds();
172
+ const plan = buildPlan(requested, targets, scope);
173
+ const canonicalDir = resolveCanonicalDir();
174
+ const projectRoot = path.resolve(opts.project);
175
+ console.log(`Scope: ${scope}${scope === "user" ? ` (${path.join(os.homedir(), ".claude")})` : ""}`);
176
+ console.log(`Targets: ${targets.join(", ")}`);
177
+ console.log(`Capabilities: ${plan.capabilityIds.join(", ")}`);
178
+ if (scope === "project")
179
+ console.log(`Project: ${projectRoot}`);
180
+ console.log(`Canonical: ${canonicalDir}`);
181
+ const allowProjectCopies = opts.allowProjectCopies === true;
182
+ // Pass 1 — preview (dry run, writes nothing).
183
+ const preview = await runPlan(plan, { scope, projectRoot, canonicalDir, dryRun: true, allowProjectCopies });
184
+ const actions = collectActions(preview);
185
+ printPlan(preview, actions, opts.dryRun);
186
+ if (opts.dryRun) {
187
+ console.log("\n[dry-run] nothing was applied.");
188
+ return;
189
+ }
190
+ if (!opts.yes) {
191
+ const proceed = await new ReadlinePrompt().confirm("\nApply these changes and run the actions above?");
192
+ if (!proceed) {
193
+ console.log("Aborted; nothing was applied.");
194
+ return;
195
+ }
196
+ }
197
+ // Pass 2 — apply file writes, record them in the install manifest, then run the actions.
198
+ // The config-guard hook denies tool writes to settings.json / hooks / CLAUDE.md without a
199
+ // marker; the installer opens its own window (never replacing or removing a user-set one)
200
+ // so an init driven from inside a Claude Code session is not blocked, and closes it after.
201
+ const closeWindow = openConfigWindow(configMarkerPath(scope, projectRoot, os.homedir()));
202
+ let results;
203
+ try {
204
+ results = await runPlan(plan, { scope, projectRoot, canonicalDir, dryRun: false, allowProjectCopies });
205
+ }
206
+ finally {
207
+ closeWindow();
208
+ }
209
+ const repo = sourceRepoRoot(canonicalDir);
210
+ // Only capabilities that emitted something are recorded: a refused global-only capability
211
+ // (project scope) leaves no trace in the project manifest.
212
+ const applied = plan.capabilityIds.filter((id) => results.some((r) => r.capabilityId === id && (r.written.length > 0 || r.actions.length > 0)));
213
+ const manifest = writeManifest({
214
+ version: cliVersion(),
215
+ sourceCommit: repo ? repoHead(repo) : undefined,
216
+ scope,
217
+ projectRoot,
218
+ targets,
219
+ capabilities: applied,
220
+ results,
221
+ actions: actions.map((a) => a.description),
222
+ });
223
+ console.log(`\nmanifest: ${manifest}`);
224
+ if (actions.length > 0)
225
+ await runActions(actions, new NodePackage());
226
+ console.log("\nApplied.");
227
+ }
228
+ /**
229
+ * Resolves the prefix budget: --budget, else FREEHARNESS_PREFIX_BUDGET, else the default.
230
+ * @param flag Parsed --budget value.
231
+ * @returns Budget in tokens.
232
+ */
233
+ function prefixBudget(flag) {
234
+ if (flag !== undefined && Number.isFinite(flag) && flag > 0)
235
+ return flag;
236
+ const env = parseInt(process.env.FREEHARNESS_PREFIX_BUDGET ?? "", 10);
237
+ return Number.isFinite(env) && env > 0 ? env : DEFAULT_PREFIX_BUDGET;
238
+ }
239
+ /**
240
+ * Fails fast when canonical does not come from a FreeHarness repo checkout (npm/npx installs
241
+ * carry no vendored engine trees or PROVENANCE to compare).
242
+ * @param command Command name for the message.
243
+ */
244
+ function requireCheckout(command) {
245
+ // The vendored trees sit three levels above canonical (packages/cli/canonical or
246
+ // canonical/ in a checkout or fixture); an npm install has nothing there.
247
+ const repo = path.resolve(resolveCanonicalDir(), "..", "..", "..");
248
+ if (["sdd", "memory"].some((t) => fs.existsSync(path.join(repo, "packages", t, "PROVENANCE.yaml"))))
249
+ return;
250
+ throw new Error(`${command} needs a FreeHarness repo checkout (this CLI runs from an npm package, not a checkout): git clone https://github.com/anegrelli/FreeHarness.git && ./install.sh, then run it from there.`);
251
+ }
252
+ /**
253
+ * Compares the manifest's recorded version/sourceCommit with the running CLI version
254
+ * and, when canonical comes from a repo checkout, with its HEAD.
255
+ * @param scope user | project.
256
+ * @param projectRoot Project root.
257
+ * @returns Report lines and whether something newer is available.
258
+ */
259
+ function checkUpdate(scope, projectRoot) {
260
+ const file = manifestPath(scope, projectRoot);
261
+ const m = readManifest(file);
262
+ const running = cliVersion();
263
+ const repo = sourceRepoRoot(resolveCanonicalDir());
264
+ const head = repo ? repoHead(repo) : undefined;
265
+ if (!m)
266
+ return { lines: [`no install manifest at ${file} (nothing installed here yet, or installed before manifests existed): run init`], newer: false };
267
+ const cmp = compareSemver(m.version, running);
268
+ const lines = [];
269
+ let newer = false;
270
+ if (cmp < 0) {
271
+ lines.push(`newer available: ${m.version} -> ${running} (CLI)`);
272
+ newer = true;
273
+ }
274
+ else if (cmp > 0) {
275
+ lines.push(`WARN installed ${m.version} is newer than this CLI (${running}); upgrade the CLI before updating`);
276
+ }
277
+ else {
278
+ lines.push(`version ${running}: up to date`);
279
+ }
280
+ if (head) {
281
+ if (!m.sourceCommit)
282
+ lines.push(`source commit ${head} (manifest has none recorded; will be recorded on the next apply)`);
283
+ else if (m.sourceCommit !== head) {
284
+ lines.push(`newer available: source ${m.sourceCommit} -> ${head} (repo ${repo})`);
285
+ newer = true;
286
+ }
287
+ else
288
+ lines.push(`source commit ${head}: up to date`);
289
+ }
290
+ return { lines, newer };
291
+ }
292
+ /**
293
+ * Numeric semver comparison on major.minor.patch (pre-release tags ignored).
294
+ * @param a Left version.
295
+ * @param b Right version.
296
+ * @returns -1 when a < b, 1 when a > b, 0 when equal.
297
+ */
298
+ function compareSemver(a, b) {
299
+ const pa = a.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
300
+ const pb = b.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
301
+ for (let i = 0; i < 3; i++) {
302
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
303
+ return -1;
304
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
305
+ return 1;
306
+ }
307
+ return 0;
308
+ }
309
+ function installOptions(cmd) {
310
+ return cmd
311
+ .option("--target <list>", `comma-separated targets (${ALL_TARGETS.join(",")}); plugin = emit a Claude Code plugin dir under --project`, DEFAULT_TARGETS.join(","))
312
+ .option("--scope <scope>", "project | user (global ~/.claude)", "project")
313
+ .option("--only <list>", "comma-separated capability ids (default: required ones)")
314
+ .option("--project <dir>", "project root (project scope only)", process.cwd())
315
+ .option("--dry-run", "preview the plan and actions without applying anything", false)
316
+ .option("--yes", "apply without interactive confirmation", false)
317
+ .option("--allow-project-copies", "project scope: also copy the global-only capabilities (skills, agents, safety, cost, memory, mcp) into the project; they are otherwise refused with a note", false);
318
+ }
319
+ installOptions(program.command("init").description("Install FreeHarness capabilities into the target harness(es); --profile applies an opt-in profile instead (local-ollama, local-llamacpp, mcp; none removes them)"))
320
+ .option("--profile <name>", `opt-in profile (${PROFILES.map((p) => p.id).join(", ")}, ${PROFILE_NONE}); applies the profile only, never the capability plan`)
321
+ .option("--select <list>", "mcp profile: comma-separated server names to register without the prompt (empty = none)")
322
+ .action(async (opts) => {
323
+ if (opts.profile)
324
+ await applyProfile(opts);
325
+ else
326
+ await applyPlan(opts);
327
+ });
328
+ const plugin = program.command("plugin").description("Claude Code plugin helpers");
329
+ plugin
330
+ .command("validate <dir>")
331
+ .description("Schema check of an emitted plugin dir (plugin.json fields, hooks.json shape and paths, .mcp.json, agent/skill front matter, sibling marketplace.json); exit 1 on errors. The `claude plugin validate` fallback for CI runners without the claude CLI")
332
+ .option("--strict", "warnings fail too", false)
333
+ .action((dir, opts) => {
334
+ const v = validatePluginDir(dir, { strict: opts.strict });
335
+ for (const l of formatValidation(path.resolve(dir), v, opts.strict))
336
+ console.log(l);
337
+ if (!v.ok)
338
+ process.exit(1);
339
+ });
340
+ installOptions(program.command("update").description("Detect a newer FreeHarness (CLI version / repo HEAD vs the install manifest) and re-emit installed capabilities (idempotent)"))
341
+ .option("--check", "report only: compare the manifest with the running CLI, apply nothing", false)
342
+ .action(async (opts) => {
343
+ const scope = parseScope(opts.scope);
344
+ const check = checkUpdate(scope, path.resolve(opts.project));
345
+ for (const l of check.lines)
346
+ console.log(`update: ${l}`);
347
+ if (opts.check)
348
+ return;
349
+ console.log(check.newer ? "\nRe-running the install from the newer source:" : "\nRe-emitting anyway (idempotent):");
350
+ await applyPlan(opts);
351
+ });
352
+ program
353
+ .command("uninstall")
354
+ .description("Remove what FreeHarness emitted into a scope (install manifest entries only); never claude-mem data or user files")
355
+ .option("--scope <scope>", "project | user (global ~/.claude)", "project")
356
+ .option("--project <dir>", "project root (project scope only)", process.cwd())
357
+ .option("--dry-run", "list what would be removed without removing anything", false)
358
+ .option("--yes", "remove without interactive confirmation", false)
359
+ .action(async (opts) => {
360
+ const scope = parseScope(opts.scope);
361
+ const projectRoot = path.resolve(opts.project);
362
+ const preview = uninstallScope(scope, projectRoot, true);
363
+ if (!preview) {
364
+ console.log(`No install manifest at ${manifestPath(scope, projectRoot)} — nothing to uninstall (installs made before manifests existed must be removed by hand).`);
365
+ return;
366
+ }
367
+ const tag = opts.dryRun ? "[dry-run] " : "";
368
+ console.log(`${tag}uninstall (${scope}): manifest ${preview.manifest}`);
369
+ console.log(`${tag}remove (${preview.remove.length}):`);
370
+ for (const p of preview.remove)
371
+ console.log(` rm: ${p}`);
372
+ for (const s of preview.settings)
373
+ console.log(` edit: ${s} (remove FreeHarness hook entries only)`);
374
+ for (const k of preview.kept)
375
+ console.log(` keep: ${k.path} — ${k.reason}`);
376
+ for (const m of preview.missing)
377
+ console.log(` gone: ${m}`);
378
+ console.log("Never touched: settings.json (beyond our hooks), the claude-mem plugin cache and data dir, files not in the manifest.");
379
+ if (opts.dryRun) {
380
+ console.log("\n[dry-run] nothing was removed.");
381
+ return;
382
+ }
383
+ if (!opts.yes && !(await new ReadlinePrompt().confirm(`\nRemove ${preview.remove.length} file(s) and prune empty dirs?`))) {
384
+ console.log("Aborted; nothing was removed.");
385
+ return;
386
+ }
387
+ const done = uninstallScope(scope, projectRoot, false);
388
+ for (const d of done.prunedDirs)
389
+ console.log(` pruned empty dir: ${d}`);
390
+ console.log(`\nRemoved ${done.remove.length} file(s).`);
391
+ });
392
+ program
393
+ .command("bootstrap")
394
+ .description("Activate FreeHarness in a project: CLAUDE.md (template, if absent) + /fh-context command + SDD (openspec/, /opsx, openspec-* skills). Skills, agents, hooks, memory and MCP stay global")
395
+ .option("--target <list>", "comma-separated harnesses", "claude-code")
396
+ .option("--project <dir>", "project root", process.cwd())
397
+ .option("--dry-run", "preview without applying", false)
398
+ .option("--yes", "apply without confirmation", false)
399
+ .action(async (opts) => {
400
+ // Project activation writes context + SDD only; everything else is installed once per
401
+ // machine (install.sh / init --scope user) and would be loaded twice from a project copy.
402
+ await applyPlan({ target: opts.target, scope: "project", only: "rules,sdd", project: opts.project, dryRun: opts.dryRun, yes: opts.yes });
403
+ console.log("note: skills, agents, hooks, memory and MCP servers are global (~/.claude); nothing per project.");
404
+ if (!opts.dryRun)
405
+ console.log(`next: open Claude Code in ${path.resolve(opts.project)} and run /fh-context to fill CLAUDE.md (never /init); commit CLAUDE.md + openspec/.`);
406
+ });
407
+ /**
408
+ * Whether a project CLAUDE.md still carries template placeholders (`<text with spaces>` or
409
+ * `<dir_name>`); HTML comments and the section tags are not placeholders.
410
+ * @param text File content.
411
+ * @returns True when at least one placeholder remains.
412
+ */
413
+ export function hasPlaceholders(text) {
414
+ const body = text.replace(/<!--[\s\S]*?-->/g, "");
415
+ return /<(?:[A-Za-z0-9][^<>\n]*\s[^<>\n]*|dir_name)>/.test(body);
416
+ }
417
+ /**
418
+ * Prints a cleanup plan for the preview / dry-run.
419
+ * @param plan The plan.
420
+ * @param tag Line prefix ("[dry-run] " or "").
421
+ */
422
+ function printCleanup(plan, tag) {
423
+ const items = plan.items;
424
+ console.log(`${tag}cleanup (${plan.scope}): ${items.length} item(s)`);
425
+ for (const i of items)
426
+ console.log(` ${i.group === "backups" ? "backup" : i.refused ? "keep " : "remove"}: ${describeItem(i)}`);
427
+ for (const n of plan.notes)
428
+ console.log(` note: ${n}`);
429
+ }
430
+ program
431
+ .command("cleanup")
432
+ .description("Remove leftovers of earlier installs: user = parked upstream claude-mem, its backups, upstream claude-mem hook entries in settings.json (plugin registered), legacy /openspec commands, global @fission-ai/openspec, a global npm freeharness link; project = legacy /openspec commands, project copies of global agents/skills, duplicate hook entries. Fixed list; nothing else is touched (your own skills/agents are never listed)")
433
+ .option("--scope <scope>", "project | user (global ~/.claude)", "project")
434
+ .option("--project <dir>", "project root (project scope only)", process.cwd())
435
+ .option("--dry-run", "list what would be removed without removing anything", false)
436
+ .option("--yes", "remove without interactive confirmation (backups included)", false)
437
+ .action(async (opts) => {
438
+ const scope = parseScope(opts.scope);
439
+ const projectRoot = path.resolve(opts.project);
440
+ const plan = await planCleanup(scope, projectRoot, { canonicalDir: resolveCanonicalDir() });
441
+ const tag = opts.dryRun ? "[dry-run] " : "";
442
+ if (plan.items.length === 0) {
443
+ console.log(`cleanup (${scope}): nothing to do.`);
444
+ for (const n of plan.notes)
445
+ console.log(` note: ${n}`);
446
+ if (!opts.dryRun) {
447
+ const pruned = pruneManifest(scope, projectRoot);
448
+ if (pruned > 0)
449
+ console.log(` manifest: dropped ${pruned} entr${pruned === 1 ? "y" : "ies"} that no longer exist`);
450
+ }
451
+ return;
452
+ }
453
+ printCleanup(plan, tag);
454
+ if (opts.dryRun) {
455
+ console.log("\n[dry-run] nothing was removed.");
456
+ return;
457
+ }
458
+ const removable = plan.items.filter((i) => !i.refused);
459
+ const main = removable.filter((i) => i.group !== "backups");
460
+ const backups = removable.filter((i) => i.group === "backups");
461
+ let confirmMain = opts.yes;
462
+ let confirmBackups = opts.yes;
463
+ if (!opts.yes) {
464
+ const prompt = new ReadlinePrompt();
465
+ if (main.length > 0)
466
+ confirmMain = await prompt.confirm(`\nRemove the ${main.length} item(s) above (backups excluded)?`);
467
+ if (backups.length > 0) {
468
+ const total = backups.reduce((n, b) => n + (b.bytes ?? 0), 0);
469
+ confirmBackups = await prompt.confirm(`Also delete the ${backups.length} claude-mem backup dir(s) (${total >= 1048576 ? `${(total / 1048576).toFixed(1)} MB` : `${total} B`})? They cannot be recovered.`);
470
+ }
471
+ }
472
+ if (!confirmMain && !confirmBackups) {
473
+ console.log("Aborted; nothing was removed.");
474
+ return;
475
+ }
476
+ const done = applyCleanup(plan, (i) => (i.group === "backups" ? confirmBackups : confirmMain));
477
+ for (const r of done.removed)
478
+ console.log(` removed: ${r}`);
479
+ for (const k of done.skipped)
480
+ console.log(` kept: ${k}`);
481
+ for (const w of done.warnings)
482
+ console.log(` WARN ${w}`);
483
+ const pruned = pruneManifest(scope, path.resolve(opts.project));
484
+ if (pruned > 0)
485
+ console.log(` manifest: dropped ${pruned} entr${pruned === 1 ? "y" : "ies"} that no longer exist`);
486
+ console.log(`\nRemoved ${done.removed.length} item(s).`);
487
+ });
488
+ program
489
+ .command("doctor")
490
+ .description("Check install state; --fix re-runs missing steps; --context-budget audits language density, prefix size and cache hygiene")
491
+ .option("--scope <scope>", "project | user", "project")
492
+ .option("--project <dir>", "project root (project scope only)", process.cwd())
493
+ .option("--fix", "re-run install to repair missing pieces", false)
494
+ .option("--context-budget", "language lint over canonical and emitted content (gerund preambles, Portuguese) + CLAUDE.md lines vs 200, hook/MCP counts, prefix estimate vs budget, cache TTLs, cache invalidators; reports, never blocks", false)
495
+ .option("--budget <tokens>", `prefix token budget for --context-budget (default ${DEFAULT_PREFIX_BUDGET} or FREEHARNESS_PREFIX_BUDGET)`, (v) => parseInt(v, 10))
496
+ .action(async (opts) => {
497
+ const scope = parseScope(opts.scope);
498
+ const base = scope === "user" ? path.join(os.homedir(), ".claude") : path.resolve(opts.project);
499
+ const claudeDir = scope === "user" ? base : path.join(base, ".claude");
500
+ const rulesFile = path.join(base, "CLAUDE.md");
501
+ const ok = (m) => console.log(`OK ${m}`);
502
+ const warn = (m) => console.log(`WARN ${m}`);
503
+ // Which version is running, and which binaries the shell resolves — the first
504
+ // thing to check after upgrading over an older install.
505
+ const resolve = (tool) => {
506
+ const r = spawnSync("sh", ["-c", `command -v ${tool}`], { encoding: "utf8" });
507
+ return r.status === 0 ? r.stdout.trim() : "(not on PATH)";
508
+ };
509
+ const localBin = path.join(os.homedir(), ".local", "bin");
510
+ console.log(`freeharness ${program.version()} running from ${process.argv[1]}`);
511
+ for (const l of runningFromLines(process.argv[1], os.homedir()))
512
+ console.log(l);
513
+ const fhPath = resolve("freeharness");
514
+ const osPath = resolve("openspec");
515
+ fhPath.startsWith(localBin) ? ok(`freeharness resolves to ${fhPath}`) : warn(`freeharness resolves to ${fhPath} — not ~/.local/bin; an older install may be shadowing the new one (open a new terminal, or 'npm unlink -g freeharness')`);
516
+ osPath.startsWith(localBin) ? ok(`openspec resolves to ${osPath} (vendored)`) : warn(`openspec resolves to ${osPath} — not the vendored bin in ~/.local/bin (a global @fission-ai/openspec may be shadowing it)`);
517
+ console.log(`Scope: ${scope} (${claudeDir})`);
518
+ try {
519
+ ok(`canonical resolved: ${resolveCanonicalDir()}`);
520
+ }
521
+ catch (e) {
522
+ warn(`canonical not found: ${e instanceof Error ? e.message : String(e)}`);
523
+ }
524
+ fs.existsSync(rulesFile) ? ok("CLAUDE.md present") : warn(scope === "user" ? "CLAUDE.md missing (run init)" : "CLAUDE.md missing (run freeharness bootstrap)");
525
+ if (opts.contextBudget) {
526
+ let canonical = null;
527
+ try {
528
+ canonical = resolveCanonicalDir();
529
+ }
530
+ catch {
531
+ canonical = null;
532
+ }
533
+ for (const l of contextBudgetLines(canonical, emittedProseRoots(scope, base)))
534
+ console.log(l);
535
+ for (const l of budgetLines({ home: os.homedir(), projectRoot: path.resolve(opts.project), budget: prefixBudget(opts.budget) }))
536
+ console.log(l);
537
+ }
538
+ if (scope === "project") {
539
+ // Project activation = context + SDD; everything else is global and is only reported
540
+ // here when a duplicate exists (see cleanup).
541
+ const opsx = path.join(claudeDir, "commands", "opsx");
542
+ fs.existsSync(opsx) ? ok(`/opsx commands present: ${fs.readdirSync(opsx).length}`) : warn("/opsx commands missing (run freeharness bootstrap)");
543
+ fs.existsSync(path.join(base, FH_CONTEXT_COMMAND)) ? ok("/fh-context command present") : warn("/fh-context command missing (run freeharness bootstrap)");
544
+ fs.existsSync(path.join(base, "openspec", "config.yaml")) ? ok("openspec/ present") : warn("openspec/ missing (run freeharness bootstrap)");
545
+ for (const l of openspecChangeLines(base))
546
+ console.log(l);
547
+ if (fs.existsSync(rulesFile) && hasPlaceholders(fs.readFileSync(rulesFile, "utf8"))) {
548
+ warn("CLAUDE.md still has <placeholder> fields: run /fh-context in Claude Code");
549
+ }
550
+ const dup = await planCleanup("project", base, { canonicalDir: resolveCanonicalDir() });
551
+ for (const i of dup.items)
552
+ warn(`duplicate of the global install: ${describeItem(i)} — run freeharness cleanup --scope project`);
553
+ if (dup.items.length === 0)
554
+ ok("no project duplicates of the global install (agents, skills, hooks, legacy /openspec commands)");
555
+ for (const l of manifestLines(manifestPath(scope, base)))
556
+ console.log(l);
557
+ if (opts.fix) {
558
+ console.log("\n--fix: re-running bootstrap (claude-code) to repair missing pieces...");
559
+ await applyPlan({ target: "claude-code", scope, only: "rules,sdd", project: opts.project, dryRun: false, yes: false });
560
+ }
561
+ return;
562
+ }
563
+ const settingsPath = path.join(claudeDir, "settings.json");
564
+ if (fs.existsSync(settingsPath)) {
565
+ const raw = fs.readFileSync(settingsPath, "utf8");
566
+ raw.includes("freeharness-session-start") ? ok("FreeHarness SessionStart hook present") : warn("FreeHarness SessionStart hook not found");
567
+ raw.includes("freeharness-memory-guard") ? ok("memory-guard hook present") : warn("memory-guard hook not found");
568
+ raw.includes("freeharness-security-guidance") ? ok("security-guidance hook present") : warn("security-guidance hook not found");
569
+ for (const h of ["freeharness-config-guard", "freeharness-repeat-tool", "freeharness-precompact"])
570
+ raw.includes(h) ? ok(`${h.replace("freeharness-", "")} hook present`) : warn(`${h.replace("freeharness-", "")} hook not found (run init --only rules)`);
571
+ raw.includes("freeharness-converge") ? ok("converge hook present (armed per project by .freeharness/converge.json)") : warn("converge hook not found (run init --only sdd)");
572
+ if (raw.includes("freeharness-cost-tracker")) {
573
+ console.log(costLogLine(costLogPath()));
574
+ for (const h of ["freeharness-output-pruner", "freeharness-context-monitor"])
575
+ raw.includes(h) ? ok(`${h.replace("freeharness-", "")} hook present`) : warn(`${h.replace("freeharness-", "")} hook not found (run init --only cost)`);
576
+ }
577
+ else {
578
+ warn("cost-tracker hook not found (the cost capability is part of the default set; run init --only cost)");
579
+ }
580
+ const hooksDir = path.join(claudeDir, "freeharness", "hooks");
581
+ const missingScripts = HOOK_SCRIPTS.filter((s) => raw.includes(s) && !fs.existsSync(path.join(hooksDir, s)));
582
+ for (const s of missingScripts)
583
+ warn(`hook registered but script missing: ${path.join(hooksDir, s)} (run update)`);
584
+ const conflicts = hookConflictLines(settingsPath, memoryPluginInstalled(claudeDir));
585
+ if (conflicts.length === 0)
586
+ ok("no hook conflicts in settings.json");
587
+ for (const l of conflicts)
588
+ console.log(l);
589
+ for (const l of hookScanLines(settingsPath, os.homedir()))
590
+ console.log(l);
591
+ }
592
+ else {
593
+ warn(`${settingsPath} not found (run init)`);
594
+ }
595
+ // v4 P6: the active local-model profile (probe) and the MCP servers with tool counts.
596
+ for (const l of await profileLines(settingsPath))
597
+ console.log(l);
598
+ try {
599
+ for (const l of mcpLines(os.homedir(), path.resolve(opts.project), mcpToolCounts(resolveCanonicalDir())))
600
+ console.log(l);
601
+ }
602
+ catch {
603
+ for (const l of mcpLines(os.homedir(), path.resolve(opts.project), {}))
604
+ console.log(l);
605
+ }
606
+ // Skill directories and agent .md files only: a .DS_Store never counts as content.
607
+ const counts = installedContentCounts(claudeDir);
608
+ counts.skills >= 0 ? ok(`skills installed: ${counts.skills}`) : warn("skills dir not found");
609
+ counts.agents >= 0 ? ok(`agents installed: ${counts.agents}`) : warn("agents dir not found");
610
+ // The user's own skills/agents next to ours: named so nobody mistakes them for leftovers.
611
+ try {
612
+ for (const l of foreignContentLines(claudeDir, resolveCanonicalDir(), scope, base))
613
+ console.log(l);
614
+ }
615
+ catch {
616
+ /* canonical unresolved: reported above */
617
+ }
618
+ const safetyFile = scope === "user" ? path.join(os.homedir(), ".claude", "freeharness", "safety.md") : path.join(base, ".freeharness", "safety.md");
619
+ fs.existsSync(safetyFile) ? ok("safety.md present") : warn("safety.md not found (run init)");
620
+ for (const l of manifestLines(manifestPath(scope, base)))
621
+ console.log(l);
622
+ for (const l of depLines(probeDeps()))
623
+ console.log(l);
624
+ console.log("Upstream (PROVENANCE baseline vs fork HEAD):");
625
+ try {
626
+ for (const l of baselineLines(resolveCanonicalDir()))
627
+ console.log(` ${l}`);
628
+ }
629
+ catch (e) {
630
+ console.log(` WARN ${e instanceof Error ? e.message : String(e)}`);
631
+ }
632
+ console.log("Memory (free-mem):");
633
+ const memory = await diagnoseMemory();
634
+ for (const line of memory.lines)
635
+ console.log(` ${line}`);
636
+ const left = await planCleanup("user", base, { memory });
637
+ for (const i of left.items)
638
+ warn(`leftover of an earlier install: ${describeItem(i)} — run freeharness cleanup --scope user`);
639
+ if (left.items.length === 0)
640
+ ok("no leftovers of earlier installs (parked claude-mem, backups, upstream claude-mem hooks, legacy /openspec commands, global @fission-ai/openspec, global npm freeharness)");
641
+ if (opts.fix) {
642
+ console.log("\n--fix: re-running install (claude-code) to repair missing pieces...");
643
+ await applyPlan({ target: "claude-code", scope, project: opts.project, dryRun: false, yes: false });
644
+ }
645
+ });
646
+ const memory = program.command("memory").description("Manage the free-mem install on this machine (ours vs upstream claude-mem)");
647
+ memory
648
+ .command("status")
649
+ .description("Which memory plugin is active (free-mem vs upstream claude-mem): plugin cache versions, registered install, running worker, data dir")
650
+ .action(async () => {
651
+ for (const line of (await diagnoseMemory()).lines)
652
+ console.log(line);
653
+ });
654
+ memory
655
+ .command("restart")
656
+ .description("Restart the free-mem worker of the registered FreeHarness build (the observer-health and version-mismatch warnings point here); an armed quota cooldown is kept on purpose")
657
+ .option("--dry-run", "preview without changing anything", false)
658
+ .option("--if-running", "exit 0 quietly when no worker is listening (install.sh after a plugin upgrade); never starts one", false)
659
+ .action(async (opts) => {
660
+ const d = await diagnoseMemory();
661
+ if (opts.ifRunning && !d.worker.reachable) {
662
+ console.log(`OK no worker on :${d.worker.port}; nothing to restart (the next SessionStart starts the registered plugin's worker)`);
663
+ return;
664
+ }
665
+ const r = await restartWorker(opts.dryRun, d);
666
+ console.log(`${opts.dryRun ? "[dry-run] " : ""}${r.method === "nothing" ? "WARN" : "OK "} ${r.method}: ${r.detail}`);
667
+ if (r.method === "nothing")
668
+ process.exit(1);
669
+ });
670
+ memory
671
+ .command("backup")
672
+ .description("Copy the claude-mem data dir (SQLite + Chroma) to a timestamped sibling; the original is untouched")
673
+ .action(() => {
674
+ const dest = backupMemoryData();
675
+ console.log(`OK backup written: ${dest}`);
676
+ });
677
+ memory
678
+ .command("activate")
679
+ .description("Make the FreeHarness build (free-mem) the only active memory plugin: stop the running worker, move foreign cache versions aside (never deleted), register ours")
680
+ .option("--dry-run", "preview without changing anything", false)
681
+ .option("--yes", "apply without confirmation", false)
682
+ .action(async (opts) => {
683
+ try {
684
+ const preview = await activateOurs(true);
685
+ console.log(`${opts.dryRun ? "[dry-run] " : ""}activate plan:`);
686
+ console.log(` stop running worker: ${preview.stoppedWorker ? "yes" : "none running"}`);
687
+ for (const d of preview.disabled)
688
+ console.log(` move aside: ${d}`);
689
+ if (preview.disabled.length === 0)
690
+ console.log(" move aside: nothing foreign in the cache");
691
+ console.log(" register: installed_plugins.json -> the FreeHarness build");
692
+ if (opts.dryRun)
693
+ return;
694
+ if (!opts.yes && !(await new ReadlinePrompt().confirm("\nApply? (run `freeharness memory backup` first if you have not)"))) {
695
+ console.log("Aborted; nothing changed.");
696
+ return;
697
+ }
698
+ await activateOurs(false);
699
+ console.log("OK activated. Open a new Claude Code session: SessionStart starts the FreeHarness worker.");
700
+ }
701
+ catch (e) {
702
+ console.error(e instanceof Error ? e.message : String(e));
703
+ process.exit(1);
704
+ }
705
+ });
706
+ /**
707
+ * True when the command line carries both `--local` and `--no-local`.
708
+ *
709
+ * Args:
710
+ * argv: The process argument vector.
711
+ *
712
+ * Returns:
713
+ * True when the two contradictory flags are both present.
714
+ */
715
+ function hasBothLocalFlags(argv) {
716
+ const tokens = argv.slice(2);
717
+ return tokens.some((t) => t === "--local" || t.startsWith("--local=")) && tokens.includes("--no-local");
718
+ }
719
+ memory
720
+ .command("settings")
721
+ .description("Preview or apply the FreeHarness-recommended observer settings (model tiers, skip list, concurrency) in the claude-mem settings.json")
722
+ .option("--apply", "write the recommended settings (two-pass: without this, preview only)", false)
723
+ .option("--yes", "apply without interactive confirmation", false)
724
+ .option("--local <values...>", "switch to the local provider: CLAUDE_MEM_PROVIDER plus four CLAUDE_MEM_LOCAL_* keys, plus CLAUDE_MEM_SKIP_TOOLS narrowed so the local model observes Read, Glob and Grep again; two values, <base-url> then <model>")
725
+ .option("--no-local", "switch back to the claude provider: CLAUDE_MEM_PROVIDER=claude and CLAUDE_MEM_SKIP_TOOLS restored to the recommended default")
726
+ .action(async (opts) => {
727
+ try {
728
+ // commander resolves --local and --no-local last-wins into one option value, so a
729
+ // contradictory line would silently write whichever came last; the raw argv is the
730
+ // only place both are still visible.
731
+ if (hasBothLocalFlags(process.argv)) {
732
+ console.error("FAIL --local and --no-local cannot be combined");
733
+ process.exit(1);
734
+ }
735
+ let observerOpts;
736
+ if (Array.isArray(opts.local)) {
737
+ if (opts.local.length !== 2) {
738
+ console.error("FAIL --local needs exactly two values: <base-url> <model>");
739
+ process.exit(1);
740
+ }
741
+ observerOpts = { local: { baseUrl: opts.local[0], model: opts.local[1] } };
742
+ }
743
+ else if (opts.local === false) {
744
+ observerOpts = { noLocal: true };
745
+ }
746
+ const settingsPath = path.join(dataDir(), "settings.json");
747
+ const preview = previewObserverSettings(settingsPath, observerOpts);
748
+ if (preview.parseError)
749
+ console.log(`WARN ${settingsPath} is not valid JSON: ${preview.parseError}`);
750
+ for (const row of preview.rows) {
751
+ console.log(` ${row.key}: current=${row.current ?? "(absent)"} recommended=${row.recommended} — ${row.changed ? "would change" : "same"}`);
752
+ }
753
+ if (!opts.apply)
754
+ return;
755
+ if (!opts.yes && !(await new ReadlinePrompt().confirm("\nApply the recommended observer settings?"))) {
756
+ console.log("Aborted; nothing changed.");
757
+ return;
758
+ }
759
+ const result = applyObserverSettings(settingsPath, observerOpts);
760
+ console.log(`OK ${result.created ? "created" : "updated"} ${settingsPath}${result.backupPath ? ` (backup: ${result.backupPath})` : ""}`);
761
+ console.log("restart the worker: freeharness memory restart");
762
+ }
763
+ catch (e) {
764
+ console.error(e instanceof Error ? e.message : String(e));
765
+ process.exit(1);
766
+ }
767
+ });
768
+ const cost = program.command("cost").description("Token/cost usage recorded by the cost-tracker hook (cost capability)");
769
+ cost
770
+ .command("report")
771
+ .description("Aggregate ~/.claude/freeharness/cost/usage.jsonl per project and per model (last snapshot per session); --sinks ranks token sinks (sessions, tools, pruned bytes) per project or per task class")
772
+ .option("--days <n>", "only sessions from the last N days", (v) => parseInt(v, 10))
773
+ .option("--project <name>", "only this project (basename of the session cwd)")
774
+ .option("--sinks", "rank token sinks: sessions by tokens with tool-output share and pruned bytes, tools by output; untagged sessions counted", false)
775
+ .option("--by <group>", "with --sinks: group sessions by `task-class` (default: per session)")
776
+ .option("--top <n>", "with --sinks: rows per project", (v) => parseInt(v, 10), 10)
777
+ .option("--changes", "per change: session count, median and total tokens per sink, USD and tier mix, from the project telemetry file", false)
778
+ .option("--dir <path>", "with --changes: the project root whose .freeharness/telemetry.jsonl to read (default: the current working directory)")
779
+ .option("--agents", "per subagent name: model, dispatch count and tokens, from the cost log's by_agent maps", false)
780
+ .option("--json", "print the aggregated report as JSON", false)
781
+ .action((opts) => {
782
+ if (opts.days !== undefined && (!Number.isFinite(opts.days) || opts.days < 0)) {
783
+ console.error("--days must be a non-negative integer");
784
+ process.exit(1);
785
+ }
786
+ if (opts.by !== undefined && opts.by !== "task-class") {
787
+ console.error("--by accepts only task-class");
788
+ process.exit(1);
789
+ }
790
+ if (opts.changes) {
791
+ // --changes reads a project directory (--dir, default cwd): unlike --project, which
792
+ // filters the other reports by the session's project *name*, this is a filesystem root.
793
+ const file = telemetryPath(opts.dir);
794
+ if (!fs.existsSync(file)) {
795
+ console.log(`no telemetry file at ${file}`);
796
+ return;
797
+ }
798
+ const records = readTelemetry(file);
799
+ const rows = changes(records);
800
+ if (opts.json)
801
+ console.log(JSON.stringify({ file, skipped: records.skipped, rows }, null, 2));
802
+ else
803
+ for (const l of formatChanges(rows, file, records.skipped))
804
+ console.log(l);
805
+ return;
806
+ }
807
+ if (opts.agents) {
808
+ const log = costLogPath();
809
+ const priceFile = costPricesPath();
810
+ const prices = loadPriceTable(priceFile);
811
+ const rows = agents(readUsage(log), prices);
812
+ if (rows.length === 0) {
813
+ console.log(`no by_agent data in ${log}`);
814
+ return;
815
+ }
816
+ if (opts.json)
817
+ console.log(JSON.stringify(rows, null, 2));
818
+ else
819
+ for (const l of formatAgents(rows, { file: priceFile, loaded: Object.keys(prices).length > 0 }))
820
+ console.log(l);
821
+ return;
822
+ }
823
+ const log = costLogPath();
824
+ const records = readUsage(log);
825
+ if (opts.sinks) {
826
+ const report = sinks(records, { days: opts.days, project: opts.project, byTaskClass: opts.by === "task-class", top: opts.top }, log);
827
+ if (opts.json)
828
+ console.log(JSON.stringify(report, null, 2));
829
+ else
830
+ for (const l of formatSinks(report))
831
+ console.log(l);
832
+ return;
833
+ }
834
+ const report = aggregate(records, { days: opts.days, project: opts.project }, log);
835
+ if (opts.json)
836
+ console.log(JSON.stringify(report, null, 2));
837
+ else
838
+ for (const l of formatReport(report))
839
+ console.log(l);
840
+ });
841
+ cost
842
+ .command("prefix")
843
+ .description("Estimate the tokens the install puts in front of every request (global rules + project CLAUDE.md + agent and skill descriptions + MCP server names; words x 1.3) against a budget")
844
+ .option("--project <dir>", "project root whose CLAUDE.md and .mcp.json count", process.cwd())
845
+ .option("--budget <tokens>", `token budget (default ${DEFAULT_PREFIX_BUDGET} or FREEHARNESS_PREFIX_BUDGET)`, (v) => parseInt(v, 10))
846
+ .option("--json", "print the estimate as JSON", false)
847
+ .action((opts) => {
848
+ const home = os.homedir();
849
+ const claudeDir = path.join(home, ".claude");
850
+ const projectRoot = path.resolve(opts.project);
851
+ const est = estimatePrefix(claudeDir, [path.join(claudeDir, "CLAUDE.md"), path.join(projectRoot, "CLAUDE.md")], mcpServerNames(home, projectRoot), prefixBudget(opts.budget));
852
+ if (opts.json) {
853
+ console.log(JSON.stringify(est, null, 2));
854
+ return;
855
+ }
856
+ console.log(`prefix estimate: ~${est.tokens} tokens (budget ${est.budget}) ${est.ok ? "OK" : "FAIL"} — ${est.items.length} sources, words x 1.3`);
857
+ console.log("top contributors:");
858
+ for (const i of est.items.slice(0, 10))
859
+ console.log(` ${String(i.tokens).padStart(6)} ${i.kind.padEnd(18)} ${i.source}`);
860
+ if (!est.ok)
861
+ console.log(`over budget by ${est.tokens - est.budget} tokens: trim the largest contributors (rules first: every line rides on every request).`);
862
+ console.log(formatInjectionLine(est.injection));
863
+ });
864
+ cost
865
+ .command("prices")
866
+ .description("Print the price table path (USD per MTok per model; edit it to your contract)")
867
+ .action(() => {
868
+ const file = costPricesPath();
869
+ console.log(file);
870
+ console.log(fs.existsSync(file) ? "present — edit to your contract; `freeharness init --only cost` never overwrites it" : "not installed yet: run `freeharness init --scope user --only cost`");
871
+ });
872
+ const sync = program.command("sync").description("Track the vendored engines against upstream");
873
+ sync
874
+ .command("check <target>")
875
+ .description("Report how a vendored tree (sdd | memory) differs from the upstream fork; never merges")
876
+ .action(async (target) => {
877
+ try {
878
+ requireCheckout("sync check");
879
+ const report = await new DiffSync(resolveCanonicalDir()).check(target);
880
+ console.log(`sync check: ${report.target} (baseline ${report.baseline})`);
881
+ if (report.changedPaths.length === 0) {
882
+ console.log("OK up to date with upstream (no vendored-path changes).");
883
+ return;
884
+ }
885
+ console.log(`Changed vs upstream (${report.changedPaths.length}):`);
886
+ for (const p of report.changedPaths) {
887
+ const flag = report.divergent[p] ? "DIVERGENT" : report.contractAffected.includes(p) ? "CONTRACT " : " ";
888
+ console.log(` ${flag} ${p}${report.divergent[p] ? ` — ${report.divergent[p]}` : ""}`);
889
+ }
890
+ console.log("\nReview only — adopting a change updates the vendored files and bumps the PROVENANCE baseline (`freeharness sync adopt <target>`).");
891
+ }
892
+ catch (e) {
893
+ console.error(e instanceof Error ? e.message : String(e));
894
+ process.exit(1);
895
+ }
896
+ });
897
+ /**
898
+ * Two-pass adoption: pass 1 previews the tracked-path diffs (writes nothing), then
899
+ * (unless --dry-run) confirm and pass 2 copies upstream over vendored + bumps the baseline.
900
+ * @param target sdd | memory.
901
+ * @param opts CLI options.
902
+ */
903
+ async function adoptUpstream(target, opts) {
904
+ requireCheckout("sync adopt");
905
+ const syncPort = new DiffSync(resolveCanonicalDir());
906
+ const paths = opts.path && opts.path.length > 0 ? opts.path : undefined;
907
+ const options = { paths, forceDivergent: opts.forceDivergent };
908
+ const preview = await syncPort.adopt(target, { ...options, dryRun: true });
909
+ const check = await syncPort.check(target);
910
+ console.log(`sync adopt: ${target} (baseline ${preview.baselineBefore} -> ${preview.baselineAfter})`);
911
+ if (paths)
912
+ console.log(`Paths: ${paths.join(", ")}`);
913
+ console.log("Scope: tracked paths only (engine code is never adopted here: it is a manual, build-verified port).");
914
+ if (preview.baselineAfter !== preview.baselineBefore) {
915
+ console.log(` upstream moved ${preview.baselineBefore}..${preview.baselineAfter}; engine drift in that range is NOT covered — see the upstream-radar report (Documents/UPSTREAM-RADAR.md).`);
916
+ }
917
+ for (const w of preview.warnings)
918
+ console.log(`WARN ${w}`);
919
+ for (const label of preview.skippedDivergent) {
920
+ console.log(`\nDIVERGENT ${label} — skipped (compare by hand): ${check.divergent[label] ?? "deliberately rewritten"}`);
921
+ }
922
+ if (preview.adopted.length === 0) {
923
+ console.log(preview.skippedDivergent.length > 0 ? "\nNothing adoptable: only DIVERGENT drift (use --force-divergent to override, with care)." : "\nOK up to date with upstream on the tracked paths; nothing to adopt.");
924
+ return;
925
+ }
926
+ console.log(`\nWould adopt (${preview.adopted.length}):`);
927
+ for (const label of preview.adopted) {
928
+ const divergentReason = check.divergent[label];
929
+ const flag = divergentReason ? "DIVERGENT" : check.contractAffected.some((p) => p === label || p.startsWith(`${label}/`)) ? "CONTRACT " : " ";
930
+ console.log(` ${flag} ${label}`);
931
+ if (divergentReason)
932
+ console.log(` WARN --force-divergent: overwriting a deliberately rewritten file (${divergentReason}); re-apply the memory-slim rewrite afterwards.`);
933
+ }
934
+ console.log("");
935
+ for (const p of preview.previews) {
936
+ console.log(`==== ${p.label}`);
937
+ process.stdout.write(p.diff);
938
+ }
939
+ if (opts.dryRun) {
940
+ console.log("\n[dry-run] nothing was written.");
941
+ return;
942
+ }
943
+ if (!opts.yes) {
944
+ const proceed = await new ReadlinePrompt().confirm(`\nCopy ${preview.adopted.length} tracked path(s) from upstream and bump the PROVENANCE baseline to ${preview.baselineAfter}?`);
945
+ if (!proceed) {
946
+ console.log("Aborted; nothing was written.");
947
+ return;
948
+ }
949
+ }
950
+ const done = await syncPort.adopt(target, { ...options, dryRun: false });
951
+ for (const w of done.warnings)
952
+ console.log(`WARN ${w}`);
953
+ console.log(`\nAdopted: ${done.adopted.join(", ")}`);
954
+ console.log(`PROVENANCE baseline: ${done.baselineBefore} -> ${done.baselineAfter}`);
955
+ console.log(target === "sdd"
956
+ ? "Rebuild: `npm run build` (then `npm test`); also refresh baseline_commit in sources.yaml."
957
+ : "Rebuild: `bun run build:plugin` in packages/memory/vendor/claude-mem/engine, then `freeharness init --only memory`; also refresh baseline_commit in sources.yaml.");
958
+ }
959
+ sync
960
+ .command("adopt <target>")
961
+ .description("Adopt the upstream versions of the tracked paths (sdd | memory): preview -> confirm -> copy -> bump PROVENANCE baseline. Never touches engine code")
962
+ .option("--path <label>", "tracked entry label to adopt (repeatable); default: all tracked entries", (v, prev = []) => [...prev, v])
963
+ .option("--dry-run", "preview the diffs without writing anything", false)
964
+ .option("--yes", "adopt without interactive confirmation", false)
965
+ .option("--force-divergent", "also overwrite DIVERGENT entries (deliberately rewritten files) — loud warning", false)
966
+ .action(async (target, opts) => {
967
+ try {
968
+ await adoptUpstream(target, opts);
969
+ }
970
+ catch (e) {
971
+ console.error(e instanceof Error ? e.message : String(e));
972
+ process.exit(1);
973
+ }
974
+ });
975
+ program
976
+ .command("list")
977
+ .description("List capabilities and what is installed")
978
+ .action(() => {
979
+ for (const c of CAPABILITIES) {
980
+ const flag = c.optional ? "optional" : "required";
981
+ const delegate = c.delegatesTo ? ` (delegates: ${c.delegatesTo})` : "";
982
+ console.log(`- ${c.id} [${flag}] -> ${c.targets.join(",")}${delegate}`);
983
+ console.log(` ${c.description}`);
984
+ }
985
+ });
986
+ program.parseAsync(process.argv).catch((err) => {
987
+ console.error(err instanceof Error ? err.message : String(err));
988
+ process.exit(1);
989
+ });