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,501 @@
1
+ // `freeharness cost`: aggregates the usage log written by the cost-tracker hook
2
+ // (canonical/hooks/freeharness-cost-tracker.mjs). Every log line is a cumulative
3
+ // snapshot of one session, so aggregation keeps the LAST snapshot per session
4
+ // (keyed by transcript path, falling back to session id) before summing per project
5
+ // and per model. Pure functions over parsed lines; index.ts prints.
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ /** Usage log path: FREEHARNESS_COST_LOG when set, else ~/.claude/freeharness/cost/usage.jsonl (may not exist yet). */
10
+ export function costLogPath() {
11
+ return process.env.FREEHARNESS_COST_LOG || path.join(os.homedir(), ".claude", "freeharness", "cost", "usage.jsonl");
12
+ }
13
+ /** Installed price table path (user scope): ~/.claude/freeharness/hooks/cost-prices.json (may not exist yet). */
14
+ export function costPricesPath() {
15
+ return process.env.FREEHARNESS_COST_PRICES || path.join(os.homedir(), ".claude", "freeharness", "hooks", "cost-prices.json");
16
+ }
17
+ /** Reads and parses usage.jsonl (malformed lines skipped); records in file order, empty when the file is absent. */
18
+ export function readUsage(file) {
19
+ return readLog(file).usage;
20
+ }
21
+ /** Reads usage.jsonl into usage snapshots and pruner records (malformed lines skipped); file order, empty when the file is absent. */
22
+ export function readLog(file) {
23
+ const usage = [];
24
+ const pruned = [];
25
+ if (!fs.existsSync(file))
26
+ return { usage, pruned };
27
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
28
+ if (!line.trim())
29
+ continue;
30
+ try {
31
+ const r = JSON.parse(line);
32
+ if (!r || typeof r !== "object")
33
+ continue;
34
+ if (r.kind === "pruned")
35
+ pruned.push(r);
36
+ else if (r.usage && typeof r.usage === "object")
37
+ usage.push(r);
38
+ }
39
+ catch {
40
+ // malformed line: skipped
41
+ }
42
+ }
43
+ return { usage, pruned };
44
+ }
45
+ function emptyRow(key) {
46
+ return { key, sessions: 0, turns: 0, input: 0, output: 0, cache_read: 0, cache_write: 0, est_usd: 0 };
47
+ }
48
+ function addBucket(row, b, usd) {
49
+ row.input += Number(b.input) || 0;
50
+ row.output += Number(b.output) || 0;
51
+ row.cache_read += Number(b.cache_read) || 0;
52
+ row.cache_write += Number(b.cache_write) || 0;
53
+ row.est_usd += usd;
54
+ }
55
+ function round(n) {
56
+ return Math.round(n * 1e6) / 1e6;
57
+ }
58
+ /**
59
+ * Keeps the last snapshot per session (transcript path, else session id, else the
60
+ * line itself) so cumulative lines are not summed twice; one record per session,
61
+ * in first-seen order.
62
+ */
63
+ export function latestPerSession(records) {
64
+ const map = new Map();
65
+ let anon = 0;
66
+ for (const r of records) {
67
+ const key = r.transcript || r.session_id || `line-${anon++}`;
68
+ const prev = map.get(key);
69
+ if (!prev || (r.ts ?? "") >= (prev.ts ?? ""))
70
+ map.set(key, r);
71
+ }
72
+ return [...map.values()];
73
+ }
74
+ /** Aggregates usage lines into per-project and per-model rows plus a total; rows sorted by estimated USD descending. */
75
+ export function aggregate(records, filter = {}, log = costLogPath()) {
76
+ const now = filter.now ?? new Date();
77
+ const since = filter.days !== undefined ? new Date(now.getTime() - filter.days * 86_400_000) : undefined;
78
+ let sessions = latestPerSession(records);
79
+ if (since)
80
+ sessions = sessions.filter((r) => Date.parse(r.ts) >= since.getTime());
81
+ if (filter.project)
82
+ sessions = sessions.filter((r) => r.project === filter.project);
83
+ const byProject = new Map();
84
+ const byModel = new Map();
85
+ const total = emptyRow("total");
86
+ let last;
87
+ for (const r of sessions) {
88
+ const usd = Number(r.est_usd) || 0;
89
+ const p = byProject.get(r.project) ?? emptyRow(r.project || "(unknown)");
90
+ p.sessions += 1;
91
+ p.turns += Number(r.turns) || 0;
92
+ addBucket(p, r.usage, usd);
93
+ byProject.set(p.key, p);
94
+ total.sessions += 1;
95
+ total.turns += Number(r.turns) || 0;
96
+ addBucket(total, r.usage, usd);
97
+ const models = r.by_model && Object.keys(r.by_model).length > 0 ? Object.entries(r.by_model) : [];
98
+ for (const [model, b] of models) {
99
+ const m = byModel.get(model) ?? emptyRow(model);
100
+ m.sessions += 1;
101
+ addBucket(m, b, Number(b.est_usd) || 0);
102
+ byModel.set(model, m);
103
+ }
104
+ if (!last || r.ts > last)
105
+ last = r.ts;
106
+ }
107
+ const finish = (rows) => [...rows].map((r) => ({ ...r, est_usd: round(r.est_usd) })).sort((a, b) => b.est_usd - a.est_usd || a.key.localeCompare(b.key));
108
+ return {
109
+ log,
110
+ since: since?.toISOString(),
111
+ project: filter.project,
112
+ lines: records.length,
113
+ sessions: sessions.length,
114
+ byProject: finish(byProject.values()),
115
+ byModel: finish(byModel.values()),
116
+ total: { ...total, est_usd: round(total.est_usd) },
117
+ last,
118
+ };
119
+ }
120
+ /** Formats a token count compactly (1.2k, 3.4M). */
121
+ export function fmtTokens(n) {
122
+ if (n >= 1e6)
123
+ return `${(n / 1e6).toFixed(2)}M`;
124
+ if (n >= 1e3)
125
+ return `${(n / 1e3).toFixed(1)}k`;
126
+ return String(n);
127
+ }
128
+ /** Renders the report as aligned text tables. */
129
+ export function formatReport(report) {
130
+ const lines = [];
131
+ const scope = [report.since ? `last ${Math.round((Date.now() - Date.parse(report.since)) / 86_400_000)} days` : "all time", report.project ? `project ${report.project}` : ""].filter(Boolean).join(", ");
132
+ lines.push(`cost report (${scope}): ${report.log}`);
133
+ lines.push(`lines ${report.lines}, sessions ${report.sessions}${report.last ? `, last entry ${report.last}` : ""}`);
134
+ if (report.sessions === 0) {
135
+ lines.push("no usage recorded yet (the hook writes on Stop / SessionEnd; run `freeharness init --only cost` if it is not installed)");
136
+ return lines;
137
+ }
138
+ const table = (title, rows, showTurns) => {
139
+ lines.push("");
140
+ lines.push(title);
141
+ const head = [`${"".padEnd(28)}`, "sessions".padStart(8), showTurns ? "turns".padStart(6) : "", "input".padStart(9), "output".padStart(9), "cache_rd".padStart(9), "cache_wr".padStart(9), "est USD".padStart(10)].filter((c) => c !== "");
142
+ lines.push(` ${head.join(" ")}`);
143
+ for (const r of rows) {
144
+ const cells = [r.key.slice(0, 28).padEnd(28), String(r.sessions).padStart(8), showTurns ? String(r.turns).padStart(6) : "", fmtTokens(r.input).padStart(9), fmtTokens(r.output).padStart(9), fmtTokens(r.cache_read).padStart(9), fmtTokens(r.cache_write).padStart(9), r.est_usd.toFixed(4).padStart(10)].filter((c) => c !== "");
145
+ lines.push(` ${cells.join(" ")}`);
146
+ }
147
+ };
148
+ table("Per project:", report.byProject, true);
149
+ table("Per model:", report.byModel, false);
150
+ table("Total:", [report.total], true);
151
+ lines.push("");
152
+ lines.push("Estimates use the editable price table (`freeharness cost prices`); edit it to your contract.");
153
+ return lines;
154
+ }
155
+ /**
156
+ * Renders the `cost prefix` session-start injection as one line, apart from the main-session
157
+ * total: Claude Code injects the routing table into the session-start turn only, so its cost
158
+ * never rides every request the way the counted items do.
159
+ * @param injection The estimate's `injection` item.
160
+ * @returns The formatted line.
161
+ */
162
+ export function formatInjectionLine(injection) {
163
+ return ` ${String(injection.tokens).padStart(6)} session-start (main session only) — ${injection.source}`;
164
+ }
165
+ /** Doctor summary for the cost log: size, entries, last entry. */
166
+ export function costLogLine(file) {
167
+ if (!fs.existsSync(file))
168
+ return `OK cost-tracker installed; no usage recorded yet (${file} appears after the first Stop)`;
169
+ const records = readUsage(file);
170
+ const kb = (fs.statSync(file).size / 1024).toFixed(1);
171
+ const last = records[records.length - 1];
172
+ const sessions = latestPerSession(records).length;
173
+ return `OK cost log: ${records.length} entries (${sessions} sessions), ${kb} KB${last ? `, last ${last.ts} ${last.project} $${(Number(last.est_usd) || 0).toFixed(4)}` : ""} — ${file}`;
174
+ }
175
+ /** Tokens estimated from characters (4 chars per token). */
176
+ function charsToTokens(chars) {
177
+ return Math.round((Number(chars) || 0) / 4);
178
+ }
179
+ /**
180
+ * Ranks token sinks per project: sessions (or task classes) by total tokens with the
181
+ * tool-output share and the pruned bytes recovered, and tools by output. Sessions without a
182
+ * task class are `untagged` and counted.
183
+ */
184
+ export function sinks(records, filter = {}, log = costLogPath()) {
185
+ const now = filter.now ?? new Date();
186
+ const since = filter.days !== undefined ? new Date(now.getTime() - filter.days * 86_400_000) : undefined;
187
+ let sessions = latestPerSession(records);
188
+ if (since)
189
+ sessions = sessions.filter((r) => Date.parse(r.ts) >= since.getTime());
190
+ if (filter.project)
191
+ sessions = sessions.filter((r) => r.project === filter.project);
192
+ const top = filter.top && filter.top > 0 ? filter.top : 10;
193
+ const byProject = new Map();
194
+ let untagged = 0;
195
+ let prunedTotal = 0;
196
+ for (const r of sessions) {
197
+ const project = r.project || "(unknown)";
198
+ const cls = r.task_class && r.task_class.trim() ? r.task_class.trim() : "untagged";
199
+ if (cls === "untagged")
200
+ untagged += 1;
201
+ const p = byProject.get(project) ?? { rows: new Map(), tools: new Map(), subagentsTotal: 0, subagentSessions: 0 };
202
+ const key = filter.byTaskClass ? cls : r.session_id || r.transcript || "(unknown)";
203
+ const row = p.rows.get(key) ?? { key, task_class: cls, sessions: 0, tokens: 0, output: 0, tool_output_tokens: 0, tool_output_share: 0, pruned_bytes: 0, est_usd: 0 };
204
+ const tokens = Number(r.usage.total) || (Number(r.usage.input) || 0) + (Number(r.usage.output) || 0) + (Number(r.usage.cache_read) || 0) + (Number(r.usage.cache_write) || 0);
205
+ row.sessions += 1;
206
+ row.tokens += tokens;
207
+ row.output += Number(r.usage.output) || 0;
208
+ row.tool_output_tokens += charsToTokens(r.tool_output_chars ?? 0);
209
+ row.pruned_bytes += Number(r.pruned_bytes) || 0;
210
+ row.est_usd += Number(r.est_usd) || 0;
211
+ prunedTotal += Number(r.pruned_bytes) || 0;
212
+ p.rows.set(key, row);
213
+ for (const [tool, t] of Object.entries(r.by_tool ?? {})) {
214
+ const tr = p.tools.get(tool) ?? { tool, calls: 0, output_tokens: 0 };
215
+ tr.calls += Number(t.calls) || 0;
216
+ tr.output_tokens += charsToTokens(t.output_chars);
217
+ p.tools.set(tool, tr);
218
+ }
219
+ // Subagent tokens, tracked apart from `usage`, are never folded into it; the project
220
+ // cwd is kept too (needed below to locate the project's own telemetry.jsonl for the
221
+ // observer row).
222
+ const subTotal = Number(r.subagents?.total) || 0;
223
+ if (subTotal > 0) {
224
+ p.subagentsTotal += subTotal;
225
+ p.subagentSessions += 1;
226
+ }
227
+ if (r.cwd && (!p.latestTs || (r.ts ?? "") >= p.latestTs)) {
228
+ p.latestTs = r.ts;
229
+ p.cwd = r.cwd;
230
+ }
231
+ byProject.set(project, p);
232
+ }
233
+ const out = [];
234
+ for (const [project, p] of byProject) {
235
+ const rows = [...p.rows.values()]
236
+ .map((r) => ({ ...r, tool_output_share: r.tokens > 0 ? Math.round((r.tool_output_tokens / r.tokens) * 1000) / 10 : 0, est_usd: round(r.est_usd) }))
237
+ .sort((a, b) => b.tokens - a.tokens || a.key.localeCompare(b.key))
238
+ .slice(0, top);
239
+ const tools = [...p.tools.values()].sort((a, b) => b.output_tokens - a.output_tokens || a.tool.localeCompare(b.tool)).slice(0, top);
240
+ const notes = [];
241
+ // `subagents` and `observer` are a fixed trailer appended after the ranked slice above:
242
+ // --top bounds only the per-session (or per-task-class) rows, never these two summary rows.
243
+ if (p.subagentsTotal > 0) {
244
+ rows.push({ key: "subagents", task_class: "sink", sessions: p.subagentSessions, tokens: p.subagentsTotal, output: 0, tool_output_tokens: 0, tool_output_share: 0, pruned_bytes: 0, est_usd: 0 });
245
+ }
246
+ let observerTokens = 0;
247
+ let observerSessions = 0;
248
+ let observerFound = false;
249
+ let localTokens = 0;
250
+ let localSessions = 0;
251
+ if (p.cwd) {
252
+ const telFile = path.join(p.cwd, ".freeharness", "telemetry.jsonl");
253
+ if (fs.existsSync(telFile)) {
254
+ observerFound = true;
255
+ for (const t of readTelemetry(telFile)) {
256
+ if (t.sinks?.observer) {
257
+ observerTokens += Number(t.sinks.observer.tokens) || 0;
258
+ observerSessions += 1;
259
+ }
260
+ const local = Number(t.sinks?.observer_local?.tokens) || 0;
261
+ if (local > 0) {
262
+ localTokens += local;
263
+ localSessions += 1;
264
+ }
265
+ }
266
+ }
267
+ }
268
+ if (observerFound) {
269
+ rows.push({ key: "observer", task_class: "sink", sessions: observerSessions, tokens: observerTokens, output: 0, tool_output_tokens: 0, tool_output_share: 0, pruned_bytes: 0, est_usd: 0 });
270
+ }
271
+ else {
272
+ notes.push(`observer sink unavailable for ${project} (no project telemetry file)`);
273
+ }
274
+ // The local-provider trailer row follows `observer`, additive, and only when the project's
275
+ // telemetry actually carries local-model tokens (a session on the Claude provider only never
276
+ // grows a zero row).
277
+ if (localTokens > 0) {
278
+ rows.push({ key: "observer_local", task_class: "sink", sessions: localSessions, tokens: localTokens, output: 0, tool_output_tokens: 0, tool_output_share: 0, pruned_bytes: 0, est_usd: 0 });
279
+ }
280
+ out.push({ project, rows, tools, notes });
281
+ }
282
+ out.sort((a, b) => b.rows.reduce((n, r) => n + r.tokens, 0) - a.rows.reduce((n, r) => n + r.tokens, 0) || a.project.localeCompare(b.project));
283
+ return { log, since: since?.toISOString(), project: filter.project, by: filter.byTaskClass ? "task-class" : "session", sessions: sessions.length, untagged, pruned_bytes: prunedTotal, byProject: out };
284
+ }
285
+ /** Renders the sinks report. */
286
+ export function formatSinks(report) {
287
+ const lines = [];
288
+ const scope = [report.since ? `last ${Math.round((Date.now() - Date.parse(report.since)) / 86_400_000)} days` : "all time", report.project ? `project ${report.project}` : ""].filter(Boolean).join(", ");
289
+ lines.push(`cost sinks by ${report.by} (${scope}): ${report.log}`);
290
+ lines.push(`sessions ${report.sessions}, untagged ${report.untagged} (no Class: header in the active brief), pruned bytes recovered ${report.pruned_bytes}`);
291
+ if (report.sessions === 0) {
292
+ lines.push("no usage recorded yet (the cost-tracker hook writes on Stop / SessionEnd)");
293
+ return lines;
294
+ }
295
+ for (const p of report.byProject) {
296
+ lines.push("");
297
+ lines.push(`Project ${p.project}:`);
298
+ lines.push(` ${"".padEnd(30)} ${"class".padEnd(12)} ${"sessions".padStart(8)} ${"tokens".padStart(9)} ${"output".padStart(9)} ${"tool-out".padStart(9)} ${"share%".padStart(7)} ${"pruned".padStart(9)} ${"est USD".padStart(9)}`);
299
+ for (const r of p.rows) {
300
+ const label = r.key === "observer_local" ? "observer (local)" : r.key;
301
+ lines.push(` ${label.slice(0, 30).padEnd(30)} ${r.task_class.slice(0, 12).padEnd(12)} ${String(r.sessions).padStart(8)} ${fmtTokens(r.tokens).padStart(9)} ${fmtTokens(r.output).padStart(9)} ${fmtTokens(r.tool_output_tokens).padStart(9)} ${r.tool_output_share.toFixed(1).padStart(7)} ${String(r.pruned_bytes).padStart(9)} ${r.est_usd.toFixed(4).padStart(9)}`);
302
+ }
303
+ if (p.tools.length > 0) {
304
+ lines.push(" tools by output:");
305
+ for (const t of p.tools)
306
+ lines.push(` ${t.tool.slice(0, 28).padEnd(28)} ${String(t.calls).padStart(6)} calls ${fmtTokens(t.output_tokens).padStart(9)} tokens`);
307
+ }
308
+ for (const n of p.notes)
309
+ lines.push(` note: ${n}`);
310
+ }
311
+ lines.push("");
312
+ lines.push("tool-out = tool_result characters / 4; share% = tool-out / tokens; pruned = bytes the output pruner kept out of the context.");
313
+ return lines;
314
+ }
315
+ /**
316
+ * Reads `.freeharness/telemetry.jsonl`: a line that is not valid JSON or lacks
317
+ * `session_id` is skipped and counted on the returned array's `skipped` property; the
318
+ * last record per `session_id` wins (ties broken by later `ts`), matching the
319
+ * `latestPerSession` rule for usage.jsonl. Deduplicated records in first-seen order;
320
+ * empty (with `skipped: 0`) when the file is absent.
321
+ */
322
+ export function readTelemetry(file) {
323
+ const map = new Map();
324
+ let skipped = 0;
325
+ if (fs.existsSync(file)) {
326
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
327
+ if (!line.trim())
328
+ continue;
329
+ let rec;
330
+ try {
331
+ const parsed = JSON.parse(line);
332
+ if (parsed && typeof parsed === "object" && typeof parsed.session_id === "string" && parsed.session_id)
333
+ rec = parsed;
334
+ }
335
+ catch {
336
+ // skip: counted below
337
+ }
338
+ if (!rec) {
339
+ skipped += 1;
340
+ continue;
341
+ }
342
+ const prev = map.get(rec.session_id);
343
+ if (!prev || (rec.ts ?? "") >= (prev.ts ?? ""))
344
+ map.set(rec.session_id, rec);
345
+ }
346
+ }
347
+ const out = [...map.values()];
348
+ out.skipped = skipped;
349
+ return out;
350
+ }
351
+ /** Median (average of the two middle values when even), rounded to 6 decimals; 0 when empty. */
352
+ function median(values) {
353
+ if (values.length === 0)
354
+ return 0;
355
+ const sorted = [...values].sort((a, b) => a - b);
356
+ const mid = Math.floor(sorted.length / 2);
357
+ return sorted.length % 2 === 0 ? round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
358
+ }
359
+ /**
360
+ * Groups telemetry records by their tagged `change` (records with no `change` fall
361
+ * into `untagged`): session count, median and total main/subagent tokens, summed
362
+ * observer tokens, summed est USD, and the tier mix (`standard 2, fast 1`, tiers with
363
+ * no session omitted). One row per change, ranked by total main tokens descending.
364
+ */
365
+ export function changes(records) {
366
+ const groups = new Map();
367
+ for (const r of records) {
368
+ const key = r.change && r.change.trim() ? r.change.trim() : "untagged";
369
+ const arr = groups.get(key) ?? [];
370
+ arr.push(r);
371
+ groups.set(key, arr);
372
+ }
373
+ const rows = [];
374
+ for (const [change, recs] of groups) {
375
+ const mains = recs.map((r) => Number(r.sinks?.main?.total) || 0);
376
+ const subs = recs.map((r) => Number(r.sinks?.subagents?.total) || 0);
377
+ const totalObserver = recs.reduce((n, r) => n + (Number(r.sinks?.observer?.tokens) || 0), 0);
378
+ const est_usd = round(recs.reduce((n, r) => n + (Number(r.est_usd) || 0), 0));
379
+ const tierCounts = new Map();
380
+ for (const r of recs) {
381
+ if (!r.tier)
382
+ continue;
383
+ tierCounts.set(r.tier, (tierCounts.get(r.tier) ?? 0) + 1);
384
+ }
385
+ const tierMix = [...tierCounts.entries()]
386
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
387
+ .map(([tier, n]) => `${tier} ${n}`)
388
+ .join(", ");
389
+ rows.push({
390
+ change,
391
+ sessions: recs.length,
392
+ medianMain: median(mains),
393
+ totalMain: mains.reduce((a, b) => a + b, 0),
394
+ medianSubagents: median(subs),
395
+ totalSubagents: subs.reduce((a, b) => a + b, 0),
396
+ totalObserver,
397
+ est_usd,
398
+ tierMix,
399
+ });
400
+ }
401
+ return rows.sort((a, b) => b.totalMain - a.totalMain || a.change.localeCompare(b.change));
402
+ }
403
+ /** Renders the `--changes` report; `skipped` counts lines `readTelemetry` dropped (malformed or without `session_id`). */
404
+ export function formatChanges(rows, file, skipped) {
405
+ const lines = [];
406
+ lines.push(`cost report --changes: ${file}${skipped > 0 ? ` (${skipped} lines skipped)` : ""}`);
407
+ if (rows.length === 0) {
408
+ lines.push("no sessions recorded yet");
409
+ return lines;
410
+ }
411
+ lines.push(` ${"change".padEnd(24)} ${"sessions".padStart(8)} ${"median main".padStart(12)} ${"total main".padStart(11)} ${"median sub".padStart(11)} ${"total sub".padStart(10)} ${"observer".padStart(9)} ${"est USD".padStart(9)} tier mix`);
412
+ for (const r of rows) {
413
+ lines.push(` ${r.change.slice(0, 24).padEnd(24)} ${String(r.sessions).padStart(8)} ${fmtTokens(r.medianMain).padStart(12)} ${fmtTokens(r.totalMain).padStart(11)} ${fmtTokens(r.medianSubagents).padStart(11)} ${fmtTokens(r.totalSubagents).padStart(10)} ${fmtTokens(r.totalObserver).padStart(9)} ${r.est_usd.toFixed(4).padStart(9)} ${r.tierMix || "-"}`);
414
+ }
415
+ return lines;
416
+ }
417
+ /**
418
+ * Loads the price table for `agents()`'s USD estimate; a missing or malformed file
419
+ * yields an empty table so the estimate degrades to 0 instead of throwing.
420
+ * Mirrors `loadPrices` in canonical/hooks/freeharness-cost-tracker.mjs; kept as a
421
+ * separate copy since the hook is a standalone, dependency-free .mjs the CLI does not
422
+ * import from. Returns model name -> price row (per MTok).
423
+ */
424
+ export function loadPriceTable(file) {
425
+ try {
426
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
427
+ return parsed && typeof parsed.prices === "object" && parsed.prices ? parsed.prices : {};
428
+ }
429
+ catch {
430
+ return {};
431
+ }
432
+ }
433
+ /**
434
+ * The price row for a model: exact key, then the longest key the model name starts
435
+ * with, then `default`. Mirrors `priceFor` in canonical/hooks/freeharness-cost-tracker.mjs
436
+ * (same separate-copy rationale as `loadPriceTable`). Undefined when no row matches.
437
+ */
438
+ function priceRowFor(prices, model) {
439
+ if (prices[model])
440
+ return prices[model];
441
+ let best;
442
+ for (const key of Object.keys(prices)) {
443
+ if (key !== "default" && model.startsWith(key) && (!best || key.length > best.length))
444
+ best = key;
445
+ }
446
+ return best ? prices[best] : prices.default;
447
+ }
448
+ /**
449
+ * Ranks subagent dispatches by total tokens from each session's last `by_agent`
450
+ * snapshot: per agent name, the model with the most tokens (ties broken by name,
451
+ * mirroring the hook's `mergeAgentUsage`), `dispatches` and `messages` each summed
452
+ * across sessions from their own fields (an entry from before per-transcript merging
453
+ * carries no `dispatches` and counts as 1), tokens per bucket summed across sessions,
454
+ * and an est USD from the price table. Rows ranked by total tokens descending.
455
+ */
456
+ export function agents(records, prices = loadPriceTable(costPricesPath())) {
457
+ const sessions = latestPerSession(records);
458
+ const byName = new Map();
459
+ for (const r of sessions) {
460
+ for (const [name, a] of Object.entries(r.by_agent ?? {})) {
461
+ const row = byName.get(name) ?? { model: a.model, bestTotal: -1, dispatches: 0, messages: 0, input: 0, output: 0, cache_read: 0, cache_write: 0 };
462
+ row.dispatches += Number(a.dispatches) || 1;
463
+ row.messages += Number(a.messages) || 0;
464
+ row.input += Number(a.usage.input) || 0;
465
+ row.output += Number(a.usage.output) || 0;
466
+ row.cache_read += Number(a.usage.cache_read) || 0;
467
+ row.cache_write += Number(a.usage.cache_write) || 0;
468
+ const total = Number(a.usage.total) || 0;
469
+ if (total > row.bestTotal || (total === row.bestTotal && a.model.localeCompare(row.model) < 0)) {
470
+ row.bestTotal = total;
471
+ row.model = a.model;
472
+ }
473
+ byName.set(name, row);
474
+ }
475
+ }
476
+ const rows = [];
477
+ for (const [name, r] of byName) {
478
+ const tokens = r.input + r.output + r.cache_read + r.cache_write;
479
+ const price = priceRowFor(prices, r.model);
480
+ const per = (n, p) => (n * (Number(p) || 0)) / 1e6;
481
+ const est_usd = price ? per(r.input, price.input) + per(r.output, price.output) + per(r.cache_read, price.cache_read) + per(r.cache_write, price.cache_write) : 0;
482
+ rows.push({ name, model: r.model, dispatches: r.dispatches, messages: r.messages, input: r.input, output: r.output, cache_read: r.cache_read, cache_write: r.cache_write, tokens, est_usd: round(est_usd) });
483
+ }
484
+ return rows.sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name));
485
+ }
486
+ /** Renders the `--agents` report; `priceTable.loaded: false` prints a notice, `true` (or omitted) prints none. */
487
+ export function formatAgents(rows, priceTable = { file: costPricesPath(), loaded: true }) {
488
+ const lines = [];
489
+ lines.push("cost report --agents (ranked by total tokens):");
490
+ if (!priceTable.loaded)
491
+ lines.push(`price table not loaded (${priceTable.file}): est USD shown as 0.00`);
492
+ lines.push(` ${"agent".padEnd(30)} ${"model".padEnd(24)} ${"dispatch".padStart(8)} ${"messages".padStart(8)} ${"tokens".padStart(9)} ${"est USD".padStart(9)}`);
493
+ for (const r of rows) {
494
+ lines.push(` ${r.name.slice(0, 30).padEnd(30)} ${r.model.slice(0, 24).padEnd(24)} ${String(r.dispatches).padStart(8)} ${String(r.messages).padStart(8)} ${fmtTokens(r.tokens).padStart(9)} ${r.est_usd.toFixed(4).padStart(9)}`);
495
+ }
496
+ return lines;
497
+ }
498
+ /** Per-project telemetry file path: `<dir>/.freeharness/telemetry.jsonl` (may not exist yet); `dir` defaults to the current working directory. */
499
+ export function telemetryPath(dir = process.cwd()) {
500
+ return path.join(path.resolve(dir), ".freeharness", "telemetry.jsonl");
501
+ }