hx-cli 0.2.2__tar.gz → 0.2.3__tar.gz

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 (221) hide show
  1. {hx_cli-0.2.2 → hx_cli-0.2.3}/CHANGELOG.md +43 -1
  2. {hx_cli-0.2.2 → hx_cli-0.2.3}/PKG-INFO +131 -9
  3. {hx_cli-0.2.2 → hx_cli-0.2.3}/README.md +127 -7
  4. hx_cli-0.2.3/examples/hooks/check.sh +103 -0
  5. {hx_cli-0.2.2 → hx_cli-0.2.3}/pyproject.toml +14 -1
  6. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/__init__.py +1 -1
  7. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/agents/subagent.py +6 -0
  8. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/cli.py +14 -0
  9. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/config.py +14 -2
  10. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/loop.py +98 -7
  11. hx_cli-0.2.3/src/hx/hooks/__init__.py +6 -0
  12. hx_cli-0.2.3/src/hx/hooks/engine.py +227 -0
  13. hx_cli-0.2.3/src/hx/hooks/spec.py +80 -0
  14. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/engine.py +2 -2
  15. hx_cli-0.2.3/src/hx/tools/anchors.py +125 -0
  16. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/edit.py +124 -8
  17. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/read.py +38 -5
  18. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/registry.py +5 -0
  19. hx_cli-0.2.3/src/hx/tools/symbols.py +391 -0
  20. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/commands.py +33 -0
  21. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/renderers.py +17 -1
  22. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/agents/test_subagent.py +4 -3
  23. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_loop.py +128 -0
  24. hx_cli-0.2.3/tests/hooks/test_engine.py +205 -0
  25. hx_cli-0.2.3/tests/hooks/test_example_check.py +98 -0
  26. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_files.py +19 -3
  27. hx_cli-0.2.3/tests/tools/test_hashline.py +197 -0
  28. hx_cli-0.2.3/tests/tools/test_symbols.py +127 -0
  29. hx_cli-0.2.3/tests/tui/views/__init__.py +0 -0
  30. {hx_cli-0.2.2 → hx_cli-0.2.3}/uv.lock +64 -12
  31. {hx_cli-0.2.2 → hx_cli-0.2.3}/.github/workflows/ci.yml +0 -0
  32. {hx_cli-0.2.2 → hx_cli-0.2.3}/.gitignore +0 -0
  33. {hx_cli-0.2.2 → hx_cli-0.2.3}/install.sh +0 -0
  34. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/agents/__init__.py +0 -0
  35. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/agents/definitions.py +0 -0
  36. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/__init__.py +0 -0
  37. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/__init__.py +0 -0
  38. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/browser.py +0 -0
  39. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/callback.py +0 -0
  40. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/codex.py +0 -0
  41. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/devin.py +0 -0
  42. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/oauth/pkce.py +0 -0
  43. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/resolve.py +0 -0
  44. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/auth/store.py +0 -0
  45. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/__init__.py +0 -0
  46. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/checkpoints.py +0 -0
  47. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/compaction.py +0 -0
  48. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/context.py +0 -0
  49. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/events.py +0 -0
  50. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/lateinject.py +0 -0
  51. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/messages.py +0 -0
  52. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/session.py +0 -0
  53. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/title.py +0 -0
  54. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/core/usage.py +0 -0
  55. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/docs.py +0 -0
  56. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/frontmatter.py +0 -0
  57. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/git.py +0 -0
  58. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/keys.py +0 -0
  59. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/mcp/__init__.py +0 -0
  60. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/mcp/client.py +0 -0
  61. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/mcp/manager.py +0 -0
  62. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/net.py +0 -0
  63. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/paths.py +0 -0
  64. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/__init__.py +0 -0
  65. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/parser.py +0 -0
  66. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/permissions/sandbox.py +0 -0
  67. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/__init__.py +0 -0
  68. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/base.py +0 -0
  69. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/codex.py +0 -0
  70. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/codex_catalogue.py +0 -0
  71. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/devin.py +0 -0
  72. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/devin_catalogue.py +0 -0
  73. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/devin_wire.py +0 -0
  74. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/fake.py +0 -0
  75. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/models.py +0 -0
  76. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/openrouter.py +0 -0
  77. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/protowire.py +0 -0
  78. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/registry.py +0 -0
  79. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/providers/responses_codec.py +0 -0
  80. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/skills/__init__.py +0 -0
  81. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/skills/loader.py +0 -0
  82. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/skills/runtime.py +0 -0
  83. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/__init__.py +0 -0
  84. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/ansi.py +0 -0
  85. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/buffer.py +0 -0
  86. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/component.py +0 -0
  87. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/demo.py +0 -0
  88. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/editor.py +0 -0
  89. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/keydecode.py +0 -0
  90. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/loop.py +0 -0
  91. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/markdown.py +0 -0
  92. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/primitives.py +0 -0
  93. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/sanitize.py +0 -0
  94. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/screen.py +0 -0
  95. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/syntax.py +0 -0
  96. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/terminal.py +0 -0
  97. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/undo.py +0 -0
  98. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/width.py +0 -0
  99. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/term/word_nav.py +0 -0
  100. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/__init__.py +0 -0
  101. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/base.py +0 -0
  102. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/bash.py +0 -0
  103. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/glob.py +0 -0
  104. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/grep.py +0 -0
  105. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/output.py +0 -0
  106. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/task.py +0 -0
  107. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/todo.py +0 -0
  108. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/websearch.py +0 -0
  109. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tools/write.py +0 -0
  110. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/__init__.py +0 -0
  111. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/app.py +0 -0
  112. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/clipboard.py +0 -0
  113. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/format.py +0 -0
  114. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/fuzzy.py +0 -0
  115. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/glyphs.py +0 -0
  116. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/killring.py +0 -0
  117. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/limits.py +0 -0
  118. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/paint.py +0 -0
  119. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/roles.py +0 -0
  120. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/runtime.py +0 -0
  121. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/theme.py +0 -0
  122. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/theme_json.py +0 -0
  123. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/themes/ansi.json +0 -0
  124. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/themes/dark.json +0 -0
  125. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/themes/light.json +0 -0
  126. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/__init__.py +0 -0
  127. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/blocks.py +0 -0
  128. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/dialog.py +0 -0
  129. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/login.py +0 -0
  130. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/permission.py +0 -0
  131. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/pickers.py +0 -0
  132. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/prompt.py +0 -0
  133. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/status.py +0 -0
  134. {hx_cli-0.2.2 → hx_cli-0.2.3}/src/hx/tui/views/transcript.py +0 -0
  135. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/__init__.py +0 -0
  136. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/agents/__init__.py +0 -0
  137. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/__init__.py +0 -0
  138. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_codex_oauth.py +0 -0
  139. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_devin_oauth.py +0 -0
  140. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_pkce.py +0 -0
  141. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_resolve.py +0 -0
  142. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/auth/test_store.py +0 -0
  143. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/conftest.py +0 -0
  144. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/__init__.py +0 -0
  145. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_checkpoints.py +0 -0
  146. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_compaction.py +0 -0
  147. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_context.py +0 -0
  148. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_lateinject.py +0 -0
  149. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_rewind.py +0 -0
  150. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_session.py +0 -0
  151. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_settings_layers.py +0 -0
  152. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_settings_migration.py +0 -0
  153. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_steering.py +0 -0
  154. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_title.py +0 -0
  155. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/core/test_usage.py +0 -0
  156. {hx_cli-0.2.2/tests/mcp → hx_cli-0.2.3/tests/hooks}/__init__.py +0 -0
  157. {hx_cli-0.2.2/tests/permissions → hx_cli-0.2.3/tests/mcp}/__init__.py +0 -0
  158. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/mcp/fixtures/echo_server.py +0 -0
  159. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/mcp/test_manager.py +0 -0
  160. {hx_cli-0.2.2/tests/providers → hx_cli-0.2.3/tests/permissions}/__init__.py +0 -0
  161. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/permissions/test_engine.py +0 -0
  162. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/permissions/test_parser.py +0 -0
  163. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/permissions/test_sandbox.py +0 -0
  164. {hx_cli-0.2.2/tests/skills → hx_cli-0.2.3/tests/providers}/__init__.py +0 -0
  165. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_catalogue.py +0 -0
  166. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_codex.py +0 -0
  167. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_codex_catalogue.py +0 -0
  168. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_devin.py +0 -0
  169. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_devin_catalogue.py +0 -0
  170. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_openrouter.py +0 -0
  171. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_protowire.py +0 -0
  172. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_registry.py +0 -0
  173. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/providers/test_responses_codec.py +0 -0
  174. {hx_cli-0.2.2/tests/term → hx_cli-0.2.3/tests/skills}/__init__.py +0 -0
  175. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/skills/test_loader.py +0 -0
  176. {hx_cli-0.2.2/tests/tools → hx_cli-0.2.3/tests/term}/__init__.py +0 -0
  177. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/conftest.py +0 -0
  178. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_ansi.py +0 -0
  179. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_keydecode.py +0 -0
  180. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_loop.py +0 -0
  181. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_markdown.py +0 -0
  182. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_primitives.py +0 -0
  183. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_sanitize.py +0 -0
  184. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_screen.py +0 -0
  185. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_terminal.py +0 -0
  186. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/term/test_width.py +0 -0
  187. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_cli.py +0 -0
  188. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_docs.py +0 -0
  189. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_git.py +0 -0
  190. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_install.py +0 -0
  191. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_integration.py +0 -0
  192. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live.py +0 -0
  193. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live_codex.py +0 -0
  194. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live_devin.py +0 -0
  195. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_live_tavily.py +0 -0
  196. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_net.py +0 -0
  197. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_paths.py +0 -0
  198. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/test_readme.py +0 -0
  199. {hx_cli-0.2.2/tests/tui → hx_cli-0.2.3/tests/tools}/__init__.py +0 -0
  200. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_background.py +0 -0
  201. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_bash.py +0 -0
  202. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_output.py +0 -0
  203. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_todo.py +0 -0
  204. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tools/test_websearch.py +0 -0
  205. {hx_cli-0.2.2/tests/tui/views → hx_cli-0.2.3/tests/tui}/__init__.py +0 -0
  206. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/support.py +0 -0
  207. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_autocomplete.py +0 -0
  208. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_clipboard.py +0 -0
  209. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_effort.py +0 -0
  210. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_keys.py +0 -0
  211. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_login.py +0 -0
  212. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_renderers.py +0 -0
  213. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_session.py +0 -0
  214. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/test_theme_json.py +0 -0
  215. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_blocks.py +0 -0
  216. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_permission.py +0 -0
  217. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_pickers.py +0 -0
  218. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_prompt.py +0 -0
  219. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_runtime.py +0 -0
  220. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_status.py +0 -0
  221. {hx_cli-0.2.2 → hx_cli-0.2.3}/tests/tui/views/test_transcript.py +0 -0
@@ -28,6 +28,47 @@ the project follows [semantic versioning](https://semver.org/).
28
28
 
29
29
  ---
30
30
 
31
+ ## [0.2.3] - 2026-09-18
32
+
33
+ ### Added
34
+
35
+ - Hooks: shell commands HX runs at `PreToolUse`, `PostToolUse`,
36
+ `UserPromptSubmit` and `Stop`. A hook can refuse an action (exit 2, or
37
+ `{"decision": "block"}`), rewrite a tool's arguments before the permission
38
+ engine sees them (`updatedInput`), or add a note for the model
39
+ (`additionalContext`). Configure them under `"hooks"` in
40
+ `~/.hx/settings.json`; `/hooks` lists what is loaded. Hooks declared in a
41
+ project's checked-in `.hx/settings.json` are ignored and reported, because
42
+ cloning a repository must not be enough to run commands on your machine.
43
+ - `Symbols`: outline a file, find where a name is defined, or find where it is
44
+ used, over a tree-sitter parse tree rather than a regex - so a match inside a
45
+ comment or a string never counts. Python, TypeScript, JavaScript, Go and
46
+ Rust. Needs the grammars: `uv tool install "hx-cli[symbols]"`. Without them
47
+ the tool is not registered at all.
48
+ - An example hook, `examples/hooks/check.sh`: after every `Edit`/`Write` it
49
+ runs the project's own checker (`ruff`, `tsc`, `go vet`, `cargo check`)
50
+ against just that file and hands the output back to the model, so a typo is
51
+ caught on the next turn instead of at test time.
52
+ - Line anchors. `Read` now labels each line with a short content hash, and
53
+ `Edit` accepts those anchors in a `hashline` argument to replace a span
54
+ without retyping it. An anchor that no longer resolves means the file moved,
55
+ so the patch is rejected instead of landing at the wrong lines. Set
56
+ `"tools": {"hashline": false}` to go back to plain `cat -n` reads.
57
+
58
+ ### Removed
59
+
60
+ - The `telemetry` setting, which was parsed and never read by anything.
61
+
62
+ ### Fixed
63
+
64
+ - The README's Status section said there were two model routes; Devin made it
65
+ three in 0.2.2.
66
+ - `MultiEdit` and `NotebookEdit` were listed as mutating tools and as edit
67
+ tools in the permission engine, but no such tools exist. A rule written
68
+ against either name silently matched nothing.
69
+
70
+ ---
71
+
31
72
  ## [0.2.2] - 2026-09-17
32
73
 
33
74
  ### Added
@@ -440,7 +481,8 @@ First release, published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/)
440
481
  - `/configure` and `hx auth` for the OpenRouter key, `hx upgrade` for
441
482
  self-update, and `install.sh` bootstrapping uv with a pinned Python.
442
483
 
443
- [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.2...HEAD
484
+ [Unreleased]: https://github.com/aletisunil/hx/compare/v0.2.3...HEAD
485
+ [0.2.3]: https://github.com/aletisunil/hx/compare/v0.2.2...v0.2.3
444
486
  [0.2.2]: https://github.com/aletisunil/hx/compare/v0.2.1...v0.2.2
445
487
  [0.2.1]: https://github.com/aletisunil/hx/compare/v0.2.0...v0.2.1
446
488
  [0.2.0]: https://github.com/aletisunil/hx/compare/v0.1.9...v0.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hx-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: HX - an agent harness for the terminal
5
5
  Project-URL: Homepage, https://github.com/aletisunil/hx
6
6
  Project-URL: Issues, https://github.com/aletisunil/hx/issues
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Python: >=3.11
17
- Requires-Dist: click>=8.1
18
17
  Requires-Dist: httpx>=0.27
19
18
  Requires-Dist: markdown-it-py>=3.0
20
19
  Requires-Dist: pydantic>=2.7
@@ -31,6 +30,9 @@ Requires-Dist: pytest>=8.2; extra == 'dev'
31
30
  Requires-Dist: ruff>=0.6; extra == 'dev'
32
31
  Requires-Dist: types-pygments; extra == 'dev'
33
32
  Requires-Dist: types-pyyaml; extra == 'dev'
33
+ Provides-Extra: symbols
34
+ Requires-Dist: tree-sitter-language-pack>=0.7; extra == 'symbols'
35
+ Requires-Dist: tree-sitter>=0.23; extra == 'symbols'
34
36
  Description-Content-Type: text/markdown
35
37
 
36
38
  # HX
@@ -438,6 +440,7 @@ than being silently resolved.
438
440
  | `/skills` | installed skills |
439
441
  | `/agents` | subagent types |
440
442
  | `/mcp` | server status |
443
+ | `/hooks` | configured hooks, and any that were refused |
441
444
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
442
445
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
443
446
  | `/copy` | copy the last reply to the clipboard |
@@ -555,7 +558,6 @@ along with the `.hx/.gitignore` it added to hide it.
555
558
  {
556
559
  "theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
557
560
  "quiet_startup": false, // skip the startup header
558
- "telemetry": false,
559
561
 
560
562
  "models": {
561
563
  "model": "anthropic/claude-sonnet-4.5",
@@ -598,10 +600,18 @@ along with the `.hx/.gitignore` it added to hide it.
598
600
  "tui": {
599
601
  "enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
600
602
  "fullscreen": false // true takes the whole window (see /fullscreen)
603
+ },
604
+
605
+ "tools": {
606
+ "hashline": true // Read labels lines with content anchors Edit accepts
601
607
  }
602
608
  }
603
609
  ```
604
610
 
611
+ `hooks` is configured in the same files but is deliberately not part of this
612
+ merge: it is read per layer, and only from your own settings. See
613
+ [Extending it](#extending-it).
614
+
605
615
  Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
606
616
  `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
607
617
  `HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
@@ -762,6 +772,54 @@ rest to the session directory, and hands the model that path to grep.
762
772
 
763
773
  ---
764
774
 
775
+ ## Reading and editing code
776
+
777
+ **Line anchors.** `Read` labels every line with a short content hash:
778
+
779
+ ```
780
+ 1 a3f9 def handle(request):
781
+ 2 7c1c if request.method == "POST":
782
+ ```
783
+
784
+ The anchor is a hash of the line and its two neighbours, truncated to the
785
+ narrowest width that is unambiguous in that file. `Edit` takes those anchors
786
+ instead of retyped content:
787
+
788
+ ```json
789
+ { "file_path": "app.py", "hashline": [{ "start": "a3f9", "end": "7c1c", "new_string": "..." }] }
790
+ ```
791
+
792
+ The span is inclusive; omit `end` to replace one line, pass an empty
793
+ `new_string` to delete. Anchors are recomputed from disk when the edit runs, so
794
+ a file that moved under the model fails to resolve and the patch is rejected
795
+ rather than applied at stale coordinates. Exact-match `old_string` still works
796
+ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
797
+ thing off with `"tools": {"hashline": false}` and reads go back to plain
798
+ `cat -n`.
799
+
800
+ **Symbols** answers structural questions with a parse tree rather than a
801
+ regex — three modes, `outline` for one file's shape, `definition` for where a
802
+ name is defined, `references` for where it is used:
803
+
804
+ ```
805
+ Symbols(mode="references", symbol="handle")
806
+ ```
807
+
808
+ Because it matches identifier nodes, the same word in a comment or a string
809
+ literal never matches. It cannot resolve types, so an unrelated symbol with the
810
+ same name in another file still does; it is tighter than `Grep`, not a language
811
+ server. Python, TypeScript, JavaScript, Go and Rust.
812
+
813
+ It needs grammars that are not installed by default, and the tool is simply not
814
+ registered without them — its schema never enters the cached prefix advertising
815
+ something that cannot run:
816
+
817
+ ```sh
818
+ uv tool install "hx-cli[symbols]"
819
+ ```
820
+
821
+ ---
822
+
765
823
  ## Extending it
766
824
 
767
825
  **Skills** are directories containing `SKILL.md` with YAML frontmatter:
@@ -818,6 +876,68 @@ Or `hx mcp add local python server.py`. Tools arrive namespaced
818
876
  with a per-server timeout; one that is broken or slow logs a warning and is
819
877
  dropped rather than taking the session with it.
820
878
 
879
+ **Hooks** are shell commands HX runs at named points in a turn. Four events:
880
+ `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
881
+
882
+ ```json
883
+ {
884
+ "hooks": {
885
+ "PreToolUse": [
886
+ {
887
+ "matcher": "Bash",
888
+ "hooks": [{ "type": "command", "command": "~/.hx/guard-bash.sh", "timeout": 10 }]
889
+ }
890
+ ],
891
+ "PostToolUse": [
892
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "ruff check --quiet" }] }
893
+ ]
894
+ }
895
+ }
896
+ ```
897
+
898
+ `matcher` is a regex matched against the whole tool name; omit it, or use `*`,
899
+ to match every tool. The hook reads a JSON event on stdin — `session_id`,
900
+ `cwd`, `hook_event_name`, and `tool_name`/`tool_input` where they apply — and
901
+ answers with its exit code:
902
+
903
+ | Exit | Means |
904
+ |---|---|
905
+ | `0` | allow |
906
+ | `2` | block. stderr becomes the reason the model is given |
907
+ | anything else | the hook is broken. Reported as a notice; the action proceeds |
908
+
909
+ That last row is deliberate: a typo in a shell command must not be able to
910
+ wedge a session. Only an explicit refusal stops anything.
911
+
912
+ A hook may also print JSON on stdout: `{"decision": "block", "reason": "..."}`
913
+ refuses, `{"updatedInput": {...}}` rewrites a tool's arguments before the
914
+ permission engine sees them, and `{"additionalContext": "..."}` adds a note for
915
+ the model to read. The format matches Claude Code's, so hooks already written
916
+ against that work here unchanged.
917
+
918
+ A worked example ships in the repository:
919
+ [`examples/hooks/check.sh`](https://github.com/aletisunil/hx/blob/main/examples/hooks/check.sh)
920
+ runs the checker a project already has — `ruff`, `tsc`, `go vet`, `cargo
921
+ check` — against the one file the model just edited, and returns whatever it
922
+ says as `additionalContext`. The model sees the errors appended to its own edit
923
+ result and fixes them on the next turn, rather than discovering them at test
924
+ time. It exits 0 either way: by `PostToolUse` the write has already landed, so
925
+ refusing it would be an answer to something that already happened.
926
+
927
+ ```sh
928
+ curl -o ~/.hx/check.sh https://raw.githubusercontent.com/aletisunil/hx/main/examples/hooks/check.sh
929
+ chmod +x ~/.hx/check.sh
930
+ ```
931
+
932
+ **Where hooks may be declared is a security boundary.** They are loaded from
933
+ `~/.hx/settings.json` and `~/.hx/projects/<slug>/settings.local.json` only.
934
+ Hooks in a project's checked-in `.hx/settings.json` are ignored and listed by
935
+ `/hooks`, because that file arrives with a clone and cloning a repository must
936
+ never be enough to run commands on the machine that cloned it. To adopt a
937
+ shared hook, copy it into your own settings — a deliberate act by the person
938
+ who will run it. Subagents inherit the parent's hooks, so a guard cannot be
939
+ bypassed by delegating through `Task`.
940
+
821
941
  ---
822
942
 
823
943
  ## Development
@@ -868,9 +988,10 @@ src/hx/
868
988
  core/ loop, context assembly, compaction, late injection, sessions, usage
869
989
  auth/ credential store, OAuth flows, per-route resolution
870
990
  providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
871
- tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, WebSearch,
872
- WebFetch, output capping
991
+ tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
992
+ WebSearch, WebFetch, line anchors, output capping
873
993
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
994
+ hooks/ the four events, the trust boundary, subprocess execution
874
995
  skills/ agents/ mcp/
875
996
  keys.py keybinding registry: ids, defaults, descriptions, user overrides
876
997
  term/ the renderer: raw mode, input decoding, the differ, the screen
@@ -933,12 +1054,13 @@ streaming with prefix caching and accurate cost accounting, session persistence
933
1054
  and resume, the tool suite, the permission engine and OS sandbox, late
934
1055
  injection, compaction, output capping, skills, subagents, MCP, and the TUI.
935
1056
 
936
- Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
937
- subscription over OAuth. The model id decides which.
1057
+ Three routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1058
+ subscription over OAuth, or a Devin subscription over OAuth. The model id
1059
+ decides which.
938
1060
 
939
1061
  Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
940
1062
  from CI on a tag.
941
1063
 
942
- The one thing still unproven in CI is a live call on either route: the `live`
943
- tests exist and cover both wire formats, tool use, reasoning replay and a
1064
+ The one thing still unproven in CI is a live call on any route: the `live`
1065
+ tests exist and cover all three wire formats, tool use, reasoning replay and a
944
1066
  genuine cache hit, but they need a credential and are deselected by default.
@@ -403,6 +403,7 @@ than being silently resolved.
403
403
  | `/skills` | installed skills |
404
404
  | `/agents` | subagent types |
405
405
  | `/mcp` | server status |
406
+ | `/hooks` | configured hooks, and any that were refused |
406
407
  | `/theme [name]` | `dark`, `light`, `ansi`, or any theme in `~/.hx/themes` |
407
408
  | `/queue [steer <n>\|clear]` | messages waiting for the turn to end, and what to do with them |
408
409
  | `/copy` | copy the last reply to the clipboard |
@@ -520,7 +521,6 @@ along with the `.hx/.gitignore` it added to hide it.
520
521
  {
521
522
  "theme": "dark", // dark | light | ansi, or a file in ~/.hx/themes
522
523
  "quiet_startup": false, // skip the startup header
523
- "telemetry": false,
524
524
 
525
525
  "models": {
526
526
  "model": "anthropic/claude-sonnet-4.5",
@@ -563,10 +563,18 @@ along with the `.hx/.gitignore` it added to hide it.
563
563
  "tui": {
564
564
  "enterWhileBusy": "queue", // queue | steer - what enter does mid-turn
565
565
  "fullscreen": false // true takes the whole window (see /fullscreen)
566
+ },
567
+
568
+ "tools": {
569
+ "hashline": true // Read labels lines with content anchors Edit accepts
566
570
  }
567
571
  }
568
572
  ```
569
573
 
574
+ `hooks` is configured in the same files but is deliberately not part of this
575
+ merge: it is read per layer, and only from your own settings. See
576
+ [Extending it](#extending-it).
577
+
570
578
  Environment overrides: `HX_MODEL`, `HX_SUBAGENT_MODEL`, `HX_MAX_TOKENS`,
571
579
  `HX_PERMISSION_MODE`, `HX_SANDBOX`, `HX_COMPACT_AT`, `HX_GIT_NOTICES`,
572
580
  `HX_THEME`, `HX_QUIET_STARTUP`. Also `HX_HOME` to relocate user state, `HX_TAVILY_API_KEY`
@@ -727,6 +735,54 @@ rest to the session directory, and hands the model that path to grep.
727
735
 
728
736
  ---
729
737
 
738
+ ## Reading and editing code
739
+
740
+ **Line anchors.** `Read` labels every line with a short content hash:
741
+
742
+ ```
743
+ 1 a3f9 def handle(request):
744
+ 2 7c1c if request.method == "POST":
745
+ ```
746
+
747
+ The anchor is a hash of the line and its two neighbours, truncated to the
748
+ narrowest width that is unambiguous in that file. `Edit` takes those anchors
749
+ instead of retyped content:
750
+
751
+ ```json
752
+ { "file_path": "app.py", "hashline": [{ "start": "a3f9", "end": "7c1c", "new_string": "..." }] }
753
+ ```
754
+
755
+ The span is inclusive; omit `end` to replace one line, pass an empty
756
+ `new_string` to delete. Anchors are recomputed from disk when the edit runs, so
757
+ a file that moved under the model fails to resolve and the patch is rejected
758
+ rather than applied at stale coordinates. Exact-match `old_string` still works
759
+ and is unchanged; anchors are a second shape, not a replacement. Turn the whole
760
+ thing off with `"tools": {"hashline": false}` and reads go back to plain
761
+ `cat -n`.
762
+
763
+ **Symbols** answers structural questions with a parse tree rather than a
764
+ regex — three modes, `outline` for one file's shape, `definition` for where a
765
+ name is defined, `references` for where it is used:
766
+
767
+ ```
768
+ Symbols(mode="references", symbol="handle")
769
+ ```
770
+
771
+ Because it matches identifier nodes, the same word in a comment or a string
772
+ literal never matches. It cannot resolve types, so an unrelated symbol with the
773
+ same name in another file still does; it is tighter than `Grep`, not a language
774
+ server. Python, TypeScript, JavaScript, Go and Rust.
775
+
776
+ It needs grammars that are not installed by default, and the tool is simply not
777
+ registered without them — its schema never enters the cached prefix advertising
778
+ something that cannot run:
779
+
780
+ ```sh
781
+ uv tool install "hx-cli[symbols]"
782
+ ```
783
+
784
+ ---
785
+
730
786
  ## Extending it
731
787
 
732
788
  **Skills** are directories containing `SKILL.md` with YAML frontmatter:
@@ -783,6 +839,68 @@ Or `hx mcp add local python server.py`. Tools arrive namespaced
783
839
  with a per-server timeout; one that is broken or slow logs a warning and is
784
840
  dropped rather than taking the session with it.
785
841
 
842
+ **Hooks** are shell commands HX runs at named points in a turn. Four events:
843
+ `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`.
844
+
845
+ ```json
846
+ {
847
+ "hooks": {
848
+ "PreToolUse": [
849
+ {
850
+ "matcher": "Bash",
851
+ "hooks": [{ "type": "command", "command": "~/.hx/guard-bash.sh", "timeout": 10 }]
852
+ }
853
+ ],
854
+ "PostToolUse": [
855
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "ruff check --quiet" }] }
856
+ ]
857
+ }
858
+ }
859
+ ```
860
+
861
+ `matcher` is a regex matched against the whole tool name; omit it, or use `*`,
862
+ to match every tool. The hook reads a JSON event on stdin — `session_id`,
863
+ `cwd`, `hook_event_name`, and `tool_name`/`tool_input` where they apply — and
864
+ answers with its exit code:
865
+
866
+ | Exit | Means |
867
+ |---|---|
868
+ | `0` | allow |
869
+ | `2` | block. stderr becomes the reason the model is given |
870
+ | anything else | the hook is broken. Reported as a notice; the action proceeds |
871
+
872
+ That last row is deliberate: a typo in a shell command must not be able to
873
+ wedge a session. Only an explicit refusal stops anything.
874
+
875
+ A hook may also print JSON on stdout: `{"decision": "block", "reason": "..."}`
876
+ refuses, `{"updatedInput": {...}}` rewrites a tool's arguments before the
877
+ permission engine sees them, and `{"additionalContext": "..."}` adds a note for
878
+ the model to read. The format matches Claude Code's, so hooks already written
879
+ against that work here unchanged.
880
+
881
+ A worked example ships in the repository:
882
+ [`examples/hooks/check.sh`](https://github.com/aletisunil/hx/blob/main/examples/hooks/check.sh)
883
+ runs the checker a project already has — `ruff`, `tsc`, `go vet`, `cargo
884
+ check` — against the one file the model just edited, and returns whatever it
885
+ says as `additionalContext`. The model sees the errors appended to its own edit
886
+ result and fixes them on the next turn, rather than discovering them at test
887
+ time. It exits 0 either way: by `PostToolUse` the write has already landed, so
888
+ refusing it would be an answer to something that already happened.
889
+
890
+ ```sh
891
+ curl -o ~/.hx/check.sh https://raw.githubusercontent.com/aletisunil/hx/main/examples/hooks/check.sh
892
+ chmod +x ~/.hx/check.sh
893
+ ```
894
+
895
+ **Where hooks may be declared is a security boundary.** They are loaded from
896
+ `~/.hx/settings.json` and `~/.hx/projects/<slug>/settings.local.json` only.
897
+ Hooks in a project's checked-in `.hx/settings.json` are ignored and listed by
898
+ `/hooks`, because that file arrives with a clone and cloning a repository must
899
+ never be enough to run commands on the machine that cloned it. To adopt a
900
+ shared hook, copy it into your own settings — a deliberate act by the person
901
+ who will run it. Subagents inherit the parent's hooks, so a guard cannot be
902
+ bypassed by delegating through `Task`.
903
+
786
904
  ---
787
905
 
788
906
  ## Development
@@ -833,9 +951,10 @@ src/hx/
833
951
  core/ loop, context assembly, compaction, late injection, sessions, usage
834
952
  auth/ credential store, OAuth flows, per-route resolution
835
953
  providers/ OpenRouter, Codex, Devin, the model catalogue, a scripted provider for tests
836
- tools/ Bash, Read, Write, Edit, Glob, Grep, TodoWrite, Task, WebSearch,
837
- WebFetch, output capping
954
+ tools/ Bash, Read, Write, Edit, Glob, Grep, Symbols, TodoWrite, Task,
955
+ WebSearch, WebFetch, line anchors, output capping
838
956
  permissions/ rule engine, shell decomposition, Seatbelt/bubblewrap
957
+ hooks/ the four events, the trust boundary, subprocess execution
839
958
  skills/ agents/ mcp/
840
959
  keys.py keybinding registry: ids, defaults, descriptions, user overrides
841
960
  term/ the renderer: raw mode, input decoding, the differ, the screen
@@ -898,12 +1017,13 @@ streaming with prefix caching and accurate cost accounting, session persistence
898
1017
  and resume, the tool suite, the permission engine and OS sandbox, late
899
1018
  injection, compaction, output capping, skills, subagents, MCP, and the TUI.
900
1019
 
901
- Two routes to a model: an OpenRouter API key, or a ChatGPT Plus/Pro
902
- subscription over OAuth. The model id decides which.
1020
+ Three routes to a model: an OpenRouter API key, a ChatGPT Plus/Pro
1021
+ subscription over OAuth, or a Devin subscription over OAuth. The model id
1022
+ decides which.
903
1023
 
904
1024
  Published to PyPI as [`hx-cli`](https://pypi.org/project/hx-cli/), released
905
1025
  from CI on a tag.
906
1026
 
907
- The one thing still unproven in CI is a live call on either route: the `live`
908
- tests exist and cover both wire formats, tool use, reasoning replay and a
1027
+ The one thing still unproven in CI is a live call on any route: the `live`
1028
+ tests exist and cover all three wire formats, tool use, reasoning replay and a
909
1029
  genuine cache hit, but they need a credential and are deselected by default.
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # PostToolUse hook: check the file the model just edited, and hand whatever the
4
+ # checker says straight back to it.
5
+ #
6
+ # Without this, HX has no idea whether an edit compiles. The model writes a
7
+ # file, sees "Applied 1 edit(s)" and a diff, and finds out something broke two
8
+ # turns later when it runs the tests - or never. This closes that loop using
9
+ # the checkers the project already has, with no language server to supervise.
10
+ #
11
+ # Wire it up in ~/.hx/settings.json (hooks are loaded from your own settings
12
+ # only - see "Extending it" in the README):
13
+ #
14
+ # {
15
+ # "hooks": {
16
+ # "PostToolUse": [
17
+ # {
18
+ # "matcher": "Edit|Write",
19
+ # "hooks": [{"type": "command", "command": "~/.hx/check.sh", "timeout": 30}]
20
+ # }
21
+ # ]
22
+ # }
23
+ # }
24
+ #
25
+ # Exits 0 whether or not it finds anything. By PostToolUse the write has
26
+ # already landed, so exit 2 would refuse an edit that is on disk regardless -
27
+ # a confusing answer to something that already happened. Errors here are
28
+ # context for the next turn, not a veto.
29
+
30
+ set -uo pipefail
31
+
32
+ # Resolve a checker the way the project would run it. A bare `command -v ruff`
33
+ # finds nothing on the common setup where the tool lives in ./.venv and never
34
+ # on $PATH, and the hook then does nothing on every edit without saying why.
35
+ # Order: the project's venv, then $PATH, then `uv run` if this is a uv project.
36
+ find_tool() {
37
+ local name=$1
38
+ if [ -x "./.venv/bin/$name" ]; then
39
+ printf './.venv/bin/%s' "$name"
40
+ return 0
41
+ fi
42
+ if command -v "$name" >/dev/null 2>&1; then
43
+ printf '%s' "$name"
44
+ return 0
45
+ fi
46
+ if [ -f ./pyproject.toml ] && command -v uv >/dev/null 2>&1; then
47
+ printf 'uv run --quiet %s' "$name"
48
+ return 0
49
+ fi
50
+ return 1
51
+ }
52
+
53
+ event=$(cat)
54
+
55
+ path=$(
56
+ printf '%s' "$event" |
57
+ python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input", {}).get("file_path", ""))' \
58
+ 2>/dev/null
59
+ )
60
+
61
+ # No path, or a file that no longer exists: nothing to check, and a hook that
62
+ # complains about its own inability to run is noise on every single edit.
63
+ [ -n "$path" ] && [ -f "$path" ] || exit 0
64
+
65
+ # One file, not the project. `ruff check .` on every edit is slow and buries
66
+ # the error the model just introduced under whatever was already failing.
67
+ case "$path" in
68
+ *.py)
69
+ ruff=$(find_tool ruff) || exit 0
70
+ # shellcheck disable=SC2086 # find_tool may return "uv run --quiet ruff"
71
+ output=$($ruff check --quiet "$path" 2>&1)
72
+ ;;
73
+ *.ts | *.tsx)
74
+ command -v npx >/dev/null || exit 0
75
+ # Single-file tsc ignores the project graph, so imports it cannot resolve
76
+ # are reported as errors. Real ones still surface; treat it as a smoke
77
+ # test rather than a substitute for `tsc -p .`.
78
+ output=$(npx --no-install tsc --noEmit "$path" 2>&1)
79
+ ;;
80
+ *.go)
81
+ command -v go >/dev/null || exit 0
82
+ output=$(go vet "$(dirname "$path")" 2>&1)
83
+ ;;
84
+ *.rs)
85
+ command -v cargo >/dev/null || exit 0
86
+ output=$(cargo check --quiet --message-format short 2>&1)
87
+ ;;
88
+ *)
89
+ # An extension with no checker is silence, not a complaint. A hook that
90
+ # says something on every Markdown edit gets switched off within a day.
91
+ exit 0
92
+ ;;
93
+ esac
94
+
95
+ [ -z "$output" ] && exit 0
96
+
97
+ printf '%s' "$output" |
98
+ python3 -c '
99
+ import json, sys
100
+ print(json.dumps({
101
+ "additionalContext": "Checker output for the file you just edited:\n" + sys.stdin.read().strip()
102
+ }))
103
+ '
@@ -23,7 +23,6 @@ dependencies = [
23
23
  "httpx>=0.27",
24
24
  "pydantic>=2.7",
25
25
  "pyyaml>=6.0",
26
- "click>=8.1",
27
26
  # System trust store, so a corporate proxy CA the machine already trusts is
28
27
  # enough - certifi alone is why `hx` fails on networks where browsers work.
29
28
  "truststore>=0.10",
@@ -37,6 +36,13 @@ dependencies = [
37
36
  ]
38
37
 
39
38
  [project.optional-dependencies]
39
+ # Tree-sitter grammars for the Symbols tool. Optional because the wheels are
40
+ # large and the rest of HX works without them - the tool is simply not
41
+ # registered when the import fails.
42
+ symbols = [
43
+ "tree-sitter>=0.23",
44
+ "tree-sitter-language-pack>=0.7",
45
+ ]
40
46
  dev = [
41
47
  "pytest>=8.2",
42
48
  "pytest-asyncio>=0.23",
@@ -99,6 +105,13 @@ plugins = []
99
105
  module = ["pyte.*", "wcwidth.*", "markdown_it.*"]
100
106
  ignore_missing_imports = true
101
107
 
108
+ # The Symbols grammars are an optional extra, so the lint job does not install
109
+ # them. Type-checking must pass in the configuration users actually get - the
110
+ # one where the import fails and the tool is never registered.
111
+ [[tool.mypy.overrides]]
112
+ module = ["tree_sitter.*", "tree_sitter_language_pack.*"]
113
+ ignore_missing_imports = true
114
+
102
115
  [tool.pytest.ini_options]
103
116
  testpaths = ["tests"]
104
117
  asyncio_mode = "auto"
@@ -1,5 +1,5 @@
1
1
  """HX - an agent harness for the terminal."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -21,6 +21,7 @@ from hx.core.events import SubagentFinished, SubagentStarted
21
21
  if TYPE_CHECKING:
22
22
  from hx.config import Settings
23
23
  from hx.core.events import EventBus
24
+ from hx.hooks.engine import HookEngine
24
25
  from hx.permissions.engine import PermissionEngine
25
26
  from hx.providers.base import Provider
26
27
  from hx.providers.models import ModelRegistry
@@ -57,6 +58,7 @@ class SubagentRunner:
57
58
  models: ModelRegistry | None = None,
58
59
  parent_session_id: str | None = None,
59
60
  parent_usage: object | None = None,
61
+ hooks: HookEngine | None = None,
60
62
  ) -> None:
61
63
  self.definitions = definitions
62
64
  self.provider = provider
@@ -67,6 +69,9 @@ class SubagentRunner:
67
69
  self.models = models
68
70
  self.parent_session_id = parent_session_id
69
71
  self.parent_usage = parent_usage
72
+ self.hooks = hooks
73
+ """Shared with the parent: a hook that guards Bash must guard a subagent's
74
+ Bash too, or the guard is one delegation away from being bypassed."""
70
75
  self._active: dict[str, str] = {}
71
76
 
72
77
  async def run(self, agent_type: str, prompt: str, description: str) -> SubagentResult:
@@ -155,6 +160,7 @@ class SubagentRunner:
155
160
  bus=self.bus,
156
161
  settings=self.settings,
157
162
  model_info=self.models.get_or_default(model) if self.models else None,
163
+ hooks=self.hooks,
158
164
  )
159
165
  loop.origin = f"{definition.name} subagent"
160
166
  return loop