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,323 @@
1
+ // FreeHarness PreCompact hook (Claude Code). Runs right before the context is compacted
2
+ // (`/compact` or auto-compaction) and leaves two artifacts for the SessionStart
3
+ // re-orientation (freeharness-session-start, source=compact):
4
+ // ~/.claude/freeharness/state/<session>.precompact.md — the eight-section checkpoint
5
+ // skeleton (Primary Request, Files and Code, Errors and Fixes, Pending Jobs, Current Work,
6
+ // Next Step, Critical Context, Corrections), one `## <Section>` heading each, built
7
+ // locally from the transcript; identifiers, paths and error strings copied verbatim.
8
+ // ~/.claude/freeharness/state/<session>.fileops.json — [{path, tool, count}] from the
9
+ // Read/Edit/Write/MultiEdit/NotebookEdit tool_use blocks of the transcript.
10
+ // It also asks the running memory worker to store a session summary
11
+ // (POST /api/sessions/summarize, the same route the Stop hook uses; it queues, it does not
12
+ // return text) so the compaction point lands in memory. The worker being down is normal:
13
+ // the request is skipped, the local skeleton is still written. Never blocks, never prints,
14
+ // exit 0 on every path (a PreCompact hook cannot inject context anyway).
15
+ // Disable: FREEHARNESS_HOOK_PRECOMPACT=off. Worker call off: FREEHARNESS_PRECOMPACT_WORKER=off.
16
+ import fs from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ /** The eight checkpoint sections, in contract order (freeharness-session-start reads them). */
22
+ export const SECTIONS = ["Primary Request", "Files and Code", "Errors and Fixes", "Pending Jobs", "Current Work", "Next Step", "Critical Context", "Corrections"];
23
+
24
+ /** Tools whose calls are file operations (Read included: it is context worth re-reading). */
25
+ const FILE_TOOLS = new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
26
+
27
+ /** Transcript scan cap (bytes). */
28
+ const MAX_TRANSCRIPT_BYTES = 64 * 1024 * 1024;
29
+
30
+ /** Per-line and per-section caps so the skeleton stays a checkpoint, not a transcript copy. */
31
+ const MAX_LINE_CHARS = 400;
32
+ const MAX_ITEMS = 12;
33
+ const MAX_BODY_CHARS = 1500;
34
+
35
+ /** Worker request timeout (ms): a PreCompact hook must never hold the compaction. */
36
+ const WORKER_TIMEOUT_MS = 2000;
37
+
38
+ /** Error markers (case-insensitive) that make a tool result an "Errors and Fixes" line. */
39
+ const ERROR_RE = /\b(error|ENOENT|EACCES|EEXIST|failed|failure|Traceback|exception|not found|permission denied|cannot|exit code [1-9])\b/i;
40
+
41
+ /** User turns that read as a correction of the assistant. */
42
+ const CORRECTION_RE = /^(no[,.! ]|not (that|this|what)|wrong|don'?t |do not |stop |instead[, ]|that'?s (not|wrong)|actually[, ]|undo|revert|never )/i;
43
+
44
+ /**
45
+ * State directory (FREEHARNESS_STATE_DIR overrides).
46
+ * @returns {string} Absolute path.
47
+ */
48
+ export function stateDir() {
49
+ return process.env.FREEHARNESS_STATE_DIR || path.join(os.homedir(), ".claude", "freeharness", "state");
50
+ }
51
+
52
+ /**
53
+ * Sanitizes a session id for use as a file name.
54
+ * @param {string} id Session id.
55
+ * @returns {string} Safe name.
56
+ */
57
+ function safeId(id) {
58
+ return String(id || "").replace(/[^\w.-]/g, "_") || "unknown";
59
+ }
60
+
61
+ /**
62
+ * Trims a string to one line of at most MAX_LINE_CHARS, marking the cut.
63
+ * @param {string} s Text.
64
+ * @param {number} [max] Cap.
65
+ * @returns {string} One line.
66
+ */
67
+ function oneLine(s, max = MAX_LINE_CHARS) {
68
+ const t = String(s).replace(/\s+/g, " ").trim();
69
+ return t.length > max ? `${t.slice(0, max)} …[+${t.length - max} chars]` : t;
70
+ }
71
+
72
+ /**
73
+ * Caps a multi-line body, keeping the head.
74
+ * @param {string} s Text.
75
+ * @returns {string} Body.
76
+ */
77
+ function capBody(s) {
78
+ const t = String(s).trim();
79
+ return t.length > MAX_BODY_CHARS ? `${t.slice(0, MAX_BODY_CHARS)}\n…[+${t.length - MAX_BODY_CHARS} chars]` : t;
80
+ }
81
+
82
+ /**
83
+ * Text of a message content (string or text blocks), tool results excluded.
84
+ * @param {unknown} content Message content.
85
+ * @returns {string} Joined text.
86
+ */
87
+ function textOf(content) {
88
+ if (typeof content === "string") return content;
89
+ if (!Array.isArray(content)) return "";
90
+ return content
91
+ .filter((b) => b && b.type === "text" && typeof b.text === "string")
92
+ .map((b) => b.text)
93
+ .join("\n");
94
+ }
95
+
96
+ /**
97
+ * True for user turns that are not real prompts (system reminders, slash-command echoes,
98
+ * tool results, interruption notices).
99
+ * @param {string} text User text.
100
+ * @returns {boolean} Skip it.
101
+ */
102
+ function isMetaUserText(text) {
103
+ const t = text.trim();
104
+ return !t || t.startsWith("<") || t.startsWith("[Request interrupted") || /^Caveat: The messages below/.test(t);
105
+ }
106
+
107
+ /**
108
+ * Text of a tool_result block (string or nested text blocks).
109
+ * @param {any} block tool_result block.
110
+ * @returns {string} Text.
111
+ */
112
+ function toolResultText(block) {
113
+ if (typeof block.content === "string") return block.content;
114
+ if (Array.isArray(block.content)) return textOf(block.content);
115
+ return "";
116
+ }
117
+
118
+ /**
119
+ * Parses the transcript into the facts the skeleton needs.
120
+ * @param {string} transcript Transcript path (JSONL).
121
+ * @returns {{users:string[], assistants:string[], fileOps:{path:string,tool:string,count:number}[], errors:string[], cwds:Set<string>, branches:Set<string>} | null}
122
+ * Facts, or null when the transcript is unreadable or oversized.
123
+ */
124
+ export function parseTranscript(transcript) {
125
+ let text;
126
+ try {
127
+ if (!transcript || fs.statSync(transcript).size > MAX_TRANSCRIPT_BYTES) return null;
128
+ text = fs.readFileSync(transcript, "utf8");
129
+ } catch {
130
+ return null;
131
+ }
132
+ const users = [];
133
+ const assistants = [];
134
+ const errors = [];
135
+ const cwds = new Set();
136
+ const branches = new Set();
137
+ const ops = new Map(); // path -> Map(tool -> count)
138
+ const toolNames = new Map(); // tool_use id -> name
139
+ for (const raw of text.split("\n")) {
140
+ if (!raw.trim()) continue;
141
+ let rec;
142
+ try {
143
+ rec = JSON.parse(raw);
144
+ } catch {
145
+ continue;
146
+ }
147
+ if (!rec || typeof rec !== "object") continue;
148
+ if (typeof rec.cwd === "string" && rec.cwd) cwds.add(rec.cwd);
149
+ if (typeof rec.gitBranch === "string" && rec.gitBranch) branches.add(rec.gitBranch);
150
+ const msg = rec.message;
151
+ if (!msg || typeof msg !== "object") continue;
152
+ const role = rec.type === "user" || rec.type === "assistant" ? rec.type : msg.role;
153
+ const content = Array.isArray(msg.content) ? msg.content : [];
154
+ if (role === "assistant") {
155
+ const t = textOf(msg.content);
156
+ if (t.trim()) assistants.push(t);
157
+ for (const block of content) {
158
+ if (!block || block.type !== "tool_use") continue;
159
+ if (typeof block.id === "string") toolNames.set(block.id, block.name);
160
+ if (!FILE_TOOLS.has(block.name)) continue;
161
+ const input = block.input && typeof block.input === "object" ? block.input : {};
162
+ const file = input.file_path || input.notebook_path;
163
+ if (typeof file !== "string" || !file) continue;
164
+ const per = ops.get(file) || new Map();
165
+ per.set(block.name, (per.get(block.name) || 0) + 1);
166
+ ops.set(file, per);
167
+ }
168
+ } else if (role === "user") {
169
+ const t = textOf(msg.content);
170
+ if (!isMetaUserText(t)) users.push(t);
171
+ for (const block of content) {
172
+ if (!block || block.type !== "tool_result") continue;
173
+ const body = toolResultText(block);
174
+ if (!(block.is_error === true || ERROR_RE.test(body))) continue;
175
+ const line = body.split("\n").map((l) => l.trim()).find((l) => l && (block.is_error === true || ERROR_RE.test(l))) || body;
176
+ const tool = toolNames.get(block.tool_use_id);
177
+ errors.push(`${tool ? `${tool}: ` : ""}${oneLine(line)}`);
178
+ }
179
+ }
180
+ }
181
+ const fileOps = [];
182
+ for (const [p, per] of ops) for (const [tool, count] of per) fileOps.push({ path: p, tool, count });
183
+ return { users, assistants, fileOps, errors, cwds, branches };
184
+ }
185
+
186
+ /**
187
+ * Renders the eight-section skeleton from transcript facts.
188
+ * @param {ReturnType<typeof parseTranscript>} facts Facts (null = unreadable transcript).
189
+ * @param {Record<string, any>} payload Hook payload.
190
+ * @returns {string} Markdown with one `## <Section>` heading per section.
191
+ */
192
+ export function renderSkeleton(facts, payload) {
193
+ const body = {};
194
+ const f = facts || { users: [], assistants: [], fileOps: [], errors: [], cwds: new Set(), branches: new Set() };
195
+ const dedupe = (arr) => [...new Set(arr)];
196
+
197
+ body["Primary Request"] = f.users.length ? capBody(f.users[0]) : "";
198
+ body["Files and Code"] = f.fileOps.length
199
+ ? f.fileOps.slice(0, MAX_ITEMS * 3).map((o) => `- ${o.path} (${o.tool} x${o.count})`).join("\n")
200
+ : "";
201
+ body["Errors and Fixes"] = f.errors.length ? dedupe(f.errors).slice(-MAX_ITEMS).map((e) => `- ${e}`).join("\n") : "";
202
+ // Unchecked task lines the assistant listed most recently, if any.
203
+ const pending = [];
204
+ for (const a of f.assistants.slice(-3)) for (const l of a.split("\n")) if (/^\s*- \[ \]/.test(l)) pending.push(oneLine(l));
205
+ body["Pending Jobs"] = pending.length ? dedupe(pending).slice(0, MAX_ITEMS).join("\n") : "";
206
+ body["Current Work"] = f.assistants.length ? capBody(f.assistants[f.assistants.length - 1]) : "";
207
+ body["Next Step"] = f.users.length > 1 ? `Latest instruction from the user (verbatim):\n${capBody(f.users[f.users.length - 1])}` : "";
208
+ const critical = [];
209
+ if (typeof payload.cwd === "string" && payload.cwd) critical.push(`cwd: ${payload.cwd}`);
210
+ for (const b of f.branches) critical.push(`git branch: ${b}`);
211
+ if (typeof payload.trigger === "string") critical.push(`compaction trigger: ${payload.trigger}`);
212
+ if (typeof payload.custom_instructions === "string" && payload.custom_instructions.trim()) critical.push(`compact instructions: ${oneLine(payload.custom_instructions)}`);
213
+ body["Critical Context"] = critical.join("\n");
214
+ body["Corrections"] = dedupe(f.users.filter((u) => CORRECTION_RE.test(u.trim())).map((u) => `- ${oneLine(u)}`)).slice(-MAX_ITEMS).join("\n");
215
+
216
+ const lines = [`<!-- freeharness-precompact ${new Date().toISOString()} session=${safeId(payload.session_id)} -->`];
217
+ for (const s of SECTIONS) {
218
+ lines.push(`## ${s}`);
219
+ lines.push(body[s] && body[s].trim() ? body[s] : "(none)");
220
+ lines.push("");
221
+ }
222
+ return `${lines.join("\n").trimEnd()}\n`;
223
+ }
224
+
225
+ /**
226
+ * Mirrors the engine's data-dir resolution: CLAUDE_MEM_DATA_DIR, settings.json, ~/.claude-mem.
227
+ * @returns {string} Absolute path.
228
+ */
229
+ export function memDataDir() {
230
+ const expand = (p) => (p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p);
231
+ if (process.env.CLAUDE_MEM_DATA_DIR) return expand(process.env.CLAUDE_MEM_DATA_DIR);
232
+ const def = path.join(os.homedir(), ".claude-mem");
233
+ try {
234
+ const raw = JSON.parse(fs.readFileSync(path.join(def, "settings.json"), "utf8"));
235
+ const s = (raw && raw.env) || raw || {};
236
+ if (typeof s.CLAUDE_MEM_DATA_DIR === "string") return expand(s.CLAUDE_MEM_DATA_DIR);
237
+ } catch {
238
+ // default
239
+ }
240
+ return def;
241
+ }
242
+
243
+ /**
244
+ * Mirrors the engine's worker port: CLAUDE_MEM_WORKER_PORT (env, then settings.json), else 37700 + uid % 100.
245
+ * @returns {number} Port.
246
+ */
247
+ export function workerPort() {
248
+ const fromEnv = Number.parseInt(process.env.CLAUDE_MEM_WORKER_PORT || "", 10);
249
+ if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
250
+ try {
251
+ const raw = JSON.parse(fs.readFileSync(path.join(memDataDir(), "settings.json"), "utf8"));
252
+ const s = (raw && raw.env) || raw || {};
253
+ const p = Number.parseInt(String(s.CLAUDE_MEM_WORKER_PORT || ""), 10);
254
+ if (Number.isFinite(p) && p > 0) return p;
255
+ } catch {
256
+ // default
257
+ }
258
+ return 37700 + (((typeof process.getuid === "function" ? process.getuid() : 77) || 0) % 100);
259
+ }
260
+
261
+ /**
262
+ * Asks the worker to queue a session summary (the Stop hook's route). Best effort.
263
+ * @param {string} sessionId Content session id.
264
+ * @param {string} lastAssistant Last assistant text (may be empty).
265
+ * @returns {Promise<"queued"|"skipped"|"down">} Outcome.
266
+ */
267
+ export async function requestSummary(sessionId, lastAssistant) {
268
+ if ((process.env.FREEHARNESS_PRECOMPACT_WORKER || "").toLowerCase() === "off") return "skipped";
269
+ const url = `http://127.0.0.1:${workerPort()}/api/sessions/summarize`;
270
+ try {
271
+ const res = await fetch(url, {
272
+ method: "POST",
273
+ headers: { "content-type": "application/json" },
274
+ body: JSON.stringify({ contentSessionId: sessionId, last_assistant_message: lastAssistant, platformSource: "claude-code" }),
275
+ signal: AbortSignal.timeout(WORKER_TIMEOUT_MS),
276
+ });
277
+ return res.ok ? "queued" : "down";
278
+ } catch {
279
+ return "down";
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Runs the hook for one payload: writes the skeleton + file-op list, pings the worker.
285
+ * @param {Record<string, any>} payload Hook payload.
286
+ * @returns {Promise<{summary:string, fileops:string, worker:string}>} Paths written and the worker outcome.
287
+ */
288
+ export async function respond(payload) {
289
+ const sessionId = typeof payload.session_id === "string" && payload.session_id ? payload.session_id : "unknown";
290
+ const facts = parseTranscript(typeof payload.transcript_path === "string" ? payload.transcript_path : "");
291
+ const dir = stateDir();
292
+ fs.mkdirSync(dir, { recursive: true });
293
+ const summary = path.join(dir, `${safeId(sessionId)}.precompact.md`);
294
+ const fileops = path.join(dir, `${safeId(sessionId)}.fileops.json`);
295
+ fs.writeFileSync(summary, renderSkeleton(facts, payload), "utf8");
296
+ fs.writeFileSync(fileops, `${JSON.stringify(facts ? facts.fileOps : [])}\n`, "utf8");
297
+ const lastAssistant = facts && facts.assistants.length ? facts.assistants[facts.assistants.length - 1] : "";
298
+ const worker = await requestSummary(sessionId, lastAssistant);
299
+ return { summary, fileops, worker };
300
+ }
301
+
302
+ /** Entry point: read stdin, write the artifacts, stay silent, exit 0. */
303
+ async function main() {
304
+ if ((process.env.FREEHARNESS_HOOK_PRECOMPACT || "").toLowerCase() === "off") return;
305
+ let payload = {};
306
+ try {
307
+ const raw = fs.readFileSync(0, "utf8");
308
+ if (raw.trim()) payload = JSON.parse(raw);
309
+ } catch {
310
+ return;
311
+ }
312
+ if (!payload || typeof payload !== "object") return;
313
+ try {
314
+ const r = await respond(payload);
315
+ if (process.env.FREEHARNESS_PRECOMPACT_VERBOSE === "1") process.stderr.write(`freeharness-precompact: ${r.summary} worker=${r.worker}\n`);
316
+ } catch (e) {
317
+ process.stderr.write(`freeharness-precompact: ${e instanceof Error ? e.message : String(e)}\n`);
318
+ }
319
+ }
320
+
321
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
322
+ main().finally(() => process.exit(0));
323
+ }
@@ -0,0 +1,208 @@
1
+ // FreeHarness repeat-tool reminder (Claude Code PreToolUse hook on every tool, plus
2
+ // UserPromptSubmit as the reset). Rewritten from the dsh repeat-tool-reminder guard (MIT).
3
+ // Each call is keyed by sha256(tool + key-sorted JSON of its input); the per-session ring
4
+ // file ~/.claude/freeharness/state/<session>.json holds the last key and its run length.
5
+ // Counting happens before execution, so a call the config guard denies still counts (a model
6
+ // hammering a denied call is the loop worth breaking). At --thresholds 3,5,8 consecutive
7
+ // identical calls the hook returns an additionalContext reminder: short at the first
8
+ // threshold, detailed (tool, run length, argument preview capped by --preview-chars) at
9
+ // the later ones. Advisory only: no permissionDecision. A new user prompt clears the ring
10
+ // (a repeat across a user message is not a loop); a missing or corrupt ring starts a fresh
11
+ // chain. Thresholds are validated at load (integers >= 2, strictly ascending, no
12
+ // duplicates); an invalid list is reported on stderr and the defaults apply, never a block.
13
+ // Disable: FREEHARNESS_HOOK_REPEAT_TOOL=off.
14
+ import crypto from "node:crypto";
15
+ import fs from "node:fs";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ /** Defaults; the fragment overrides them with --thresholds / --preview-chars. */
21
+ export const DEFAULTS = Object.freeze({ thresholds: [3, 5, 8], previewChars: 500 });
22
+
23
+ /**
24
+ * Validates a threshold list: integers >= 2, strictly ascending, no duplicates.
25
+ * @param {number[]} values Candidate list.
26
+ * @returns {string | null} The rule violated, or null when valid.
27
+ */
28
+ export function validateThresholds(values) {
29
+ if (!Array.isArray(values) || values.length === 0) return "thresholds must not be empty";
30
+ for (const v of values) if (!Number.isInteger(v) || v < 2) return `threshold ${v} is not an integer >= 2`;
31
+ for (let i = 1; i < values.length; i++) if (values[i] <= values[i - 1]) return `thresholds must be strictly ascending (${values[i - 1]} then ${values[i]})`;
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * Parses --thresholds (comma list) and --preview-chars; invalid values fall back to the
37
+ * defaults with a stderr line naming the rule.
38
+ * @param {string[]} argv Arguments after the script path.
39
+ * @returns {{thresholds:number[], previewChars:number, error?:string}} Options.
40
+ */
41
+ export function parseOptions(argv) {
42
+ const opts = { thresholds: [...DEFAULTS.thresholds], previewChars: DEFAULTS.previewChars };
43
+ for (let i = 0; i < argv.length; i++) {
44
+ if (argv[i] === "--thresholds") {
45
+ const raw = String(argv[++i] ?? "");
46
+ const list = raw.split(",").map((s) => (s.trim() === "" ? NaN : Number(s.trim())));
47
+ const err = validateThresholds(list);
48
+ if (err) opts.error = `freeharness-repeat-tool: invalid thresholds "${raw}": ${err}; using ${DEFAULTS.thresholds.join(",")}`;
49
+ else opts.thresholds = list;
50
+ } else if (argv[i] === "--preview-chars") {
51
+ const n = Number.parseInt(argv[++i], 10);
52
+ if (Number.isInteger(n) && n >= 1) opts.previewChars = n;
53
+ }
54
+ }
55
+ return opts;
56
+ }
57
+
58
+ /**
59
+ * Deep key-sort so two inputs that differ only in property order canonicalize identically.
60
+ * @param {unknown} value Parsed JSON value.
61
+ * @returns {unknown} Sorted copy.
62
+ */
63
+ export function sortKeys(value) {
64
+ if (Array.isArray(value)) return value.map(sortKeys);
65
+ if (value && typeof value === "object") {
66
+ const out = {};
67
+ for (const k of Object.keys(value).sort()) out[k] = sortKeys(value[k]);
68
+ return out;
69
+ }
70
+ return value;
71
+ }
72
+
73
+ /**
74
+ * Canonical string of a call's input.
75
+ * @param {unknown} input Tool input.
76
+ * @returns {string} JSON with sorted keys.
77
+ */
78
+ export function canonical(input) {
79
+ return JSON.stringify(sortKeys(input ?? {}));
80
+ }
81
+
82
+ /**
83
+ * Call identity: sha256 over the tool name and its canonical input.
84
+ * @param {string} tool Tool name.
85
+ * @param {string} canon Canonical input.
86
+ * @returns {string} Hex digest.
87
+ */
88
+ export function callKey(tool, canon) {
89
+ return crypto.createHash("sha256").update(tool).update("\0").update(canon).digest("hex");
90
+ }
91
+
92
+ /**
93
+ * Resolves the ring file of a session.
94
+ * @param {string} sessionId Session id.
95
+ * @returns {string} Absolute path.
96
+ */
97
+ export function ringFile(sessionId) {
98
+ const dir = process.env.FREEHARNESS_STATE_DIR || path.join(os.homedir(), ".claude", "freeharness", "state");
99
+ return path.join(dir, `${sessionId.replace(/[^\w.-]/g, "_") || "unknown"}.json`);
100
+ }
101
+
102
+ /**
103
+ * Reads the ring; a missing or corrupt file is a fresh chain.
104
+ * @param {string} file Ring path.
105
+ * @returns {{key:string, count:number}} Chain state.
106
+ */
107
+ function readRing(file) {
108
+ try {
109
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
110
+ if (parsed && typeof parsed.key === "string" && Number.isInteger(parsed.count) && parsed.count >= 0) return { key: parsed.key, count: parsed.count };
111
+ } catch {
112
+ // fresh
113
+ }
114
+ return { key: "", count: 0 };
115
+ }
116
+
117
+ /**
118
+ * Writes the ring.
119
+ * @param {string} file Ring path.
120
+ * @param {{key:string, count:number}} ring State.
121
+ */
122
+ function writeRing(file, ring) {
123
+ fs.mkdirSync(path.dirname(file), { recursive: true });
124
+ fs.writeFileSync(file, JSON.stringify({ ...ring, updated: new Date().toISOString() }), "utf8");
125
+ }
126
+
127
+ /**
128
+ * Head-truncates the canonical input for the detailed reminder.
129
+ * @param {string} canon Canonical input.
130
+ * @param {number} cap Character cap.
131
+ * @returns {string} Preview.
132
+ */
133
+ function preview(canon, cap) {
134
+ return canon.length <= cap ? canon : `${canon.slice(0, cap)}… (+${canon.length - cap} more chars)`;
135
+ }
136
+
137
+ /**
138
+ * Reminder text for a run length that hit a threshold.
139
+ * @param {string} tool Tool name.
140
+ * @param {number} count Run length.
141
+ * @param {string} canon Canonical input.
142
+ * @param {{thresholds:number[], previewChars:number}} opts Options.
143
+ * @returns {string} Reminder.
144
+ */
145
+ export function reminder(tool, count, canon, opts) {
146
+ if (count === opts.thresholds[0]) {
147
+ return `FreeHarness: same ${tool} call with identical arguments ${count} times in a row. Analyze the previous result before calling again; if the task is not complete, change the approach or the arguments.`;
148
+ }
149
+ return [
150
+ "FreeHarness: repeated tool call detected.",
151
+ `- tool: ${tool}`,
152
+ `- consecutive_calls: ${count}`,
153
+ `- arguments: ${preview(canon, opts.previewChars)}`,
154
+ "The repeated calls are not making progress. Do not call this tool with these exact arguments again: inspect the latest result, choose a different action or different arguments, or finish the task if enough evidence has been gathered.",
155
+ ].join("\n");
156
+ }
157
+
158
+ /**
159
+ * Advances the chain for one PreToolUse payload and returns the reminder response, or
160
+ * null; clears the chain for a UserPromptSubmit payload.
161
+ * @param {Record<string, any>} payload Parsed hook payload.
162
+ * @param {{thresholds:number[], previewChars:number}} opts Options.
163
+ * @returns {Record<string, unknown> | null} JSON to print, or null.
164
+ */
165
+ export function respond(payload, opts) {
166
+ const sessionId = typeof payload.session_id === "string" && payload.session_id ? payload.session_id : "unknown";
167
+ const file = ringFile(sessionId);
168
+ const event = String(payload.hook_event_name || (payload.prompt !== undefined ? "UserPromptSubmit" : "PreToolUse"));
169
+ if (event === "UserPromptSubmit") {
170
+ writeRing(file, { key: "", count: 0 });
171
+ return null;
172
+ }
173
+ const tool = String(payload.tool_name || "");
174
+ if (!tool) return null;
175
+ const canon = canonical(payload.tool_input);
176
+ const key = callKey(tool, canon);
177
+ const ring = readRing(file);
178
+ const count = ring.key === key ? ring.count + 1 : 1;
179
+ writeRing(file, { key, count });
180
+ if (!opts.thresholds.includes(count)) return null;
181
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: reminder(tool, count, canon, opts) } };
182
+ }
183
+
184
+ /** Entry point: read stdin, respond or stay silent, exit 0. */
185
+ function main() {
186
+ if ((process.env.FREEHARNESS_HOOK_REPEAT_TOOL || "").toLowerCase() === "off") return;
187
+ const opts = parseOptions(process.argv.slice(2));
188
+ if (opts.error) process.stderr.write(`${opts.error}\n`);
189
+ let payload = {};
190
+ try {
191
+ const raw = fs.readFileSync(0, "utf8");
192
+ if (raw.trim()) payload = JSON.parse(raw);
193
+ } catch {
194
+ return;
195
+ }
196
+ if (!payload || typeof payload !== "object") return;
197
+ try {
198
+ const out = respond(payload, opts);
199
+ if (out) process.stdout.write(JSON.stringify(out));
200
+ } catch (e) {
201
+ process.stderr.write(`freeharness-repeat-tool: ${e instanceof Error ? e.message : String(e)}\n`);
202
+ }
203
+ }
204
+
205
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
206
+ main();
207
+ process.exit(0);
208
+ }