capt-hook 12.2.0__tar.gz → 12.3.0__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 (180) hide show
  1. {capt_hook-12.2.0 → capt_hook-12.3.0}/PKG-INFO +1 -1
  2. {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/client.py +0 -1
  3. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/.claude-plugin/plugin.json +1 -1
  4. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/__init__.py +4 -0
  5. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/__init__.pyi +4 -0
  6. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/tombstones.py +1 -1
  7. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/conditions.py +21 -4
  8. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/context.py +21 -3
  9. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/protocol.py +44 -21
  10. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/signals/nlp.py +44 -23
  11. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +4 -2
  12. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +1 -1
  13. capt_hook-12.3.0/captain_hook/turn.py +32 -0
  14. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/types.py +23 -0
  15. {capt_hook-12.2.0 → capt_hook-12.3.0}/pyproject.toml +1 -1
  16. {capt_hook-12.2.0 → capt_hook-12.3.0}/.gitignore +0 -0
  17. {capt_hook-12.2.0 → capt_hook-12.3.0}/LICENSE +0 -0
  18. {capt_hook-12.2.0 → capt_hook-12.3.0}/README.md +0 -0
  19. {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/__init__.py +0 -0
  20. {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/__main__.py +0 -0
  21. {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/key.py +0 -0
  22. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/__main__.py +0 -0
  23. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/app.py +0 -0
  24. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/ast_grep.py +0 -0
  25. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/__init__.py +0 -0
  26. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/__init__.py +0 -0
  27. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/__init__.py +0 -0
  28. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/_lib.py +0 -0
  29. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py +0 -0
  30. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py +0 -0
  31. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/__init__.py +0 -0
  32. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/__init__.py +0 -0
  33. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/_lib.py +0 -0
  34. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/commands.py +0 -0
  35. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/comments.py +0 -0
  36. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/deletions.py +0 -0
  37. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/detours.py +0 -0
  38. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/docs.py +0 -0
  39. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/models.py +0 -0
  40. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/plans.py +0 -0
  41. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/deliverable_rubric.md +0 -0
  42. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/workflow_script_header.md +0 -0
  43. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/browser_delegation_nudge.md +0 -0
  44. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/implementation_spawn_nudge.md +0 -0
  45. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/inline_edit_nudge.md +0 -0
  46. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_spawn_gate.md +0 -0
  47. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_workflow_nudge.md +0 -0
  48. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_spawn_nudge.md +0 -0
  49. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_workflow_nudge.md +0 -0
  50. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_spawn_nudge.md +0 -0
  51. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_workflow_nudge.md +0 -0
  52. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/tools/preload_nudge.md +0 -0
  53. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts.py +0 -0
  54. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/review.py +0 -0
  55. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/tasks.py +0 -0
  56. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/tools.py +0 -0
  57. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/pack.toml +0 -0
  58. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/__init__.py +0 -0
  59. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/hooks/__init__.py +0 -0
  60. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/hooks/testing.py +0 -0
  61. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/hooks/toolchain.py +0 -0
  62. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/performance/__init__.py +0 -0
  63. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/performance/hooks/__init__.py +0 -0
  64. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/performance/hooks/pipelining.py +0 -0
  65. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/__init__.py +0 -0
  66. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/__init__.py +0 -0
  67. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/style.py +0 -0
  68. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/testing.py +0 -0
  69. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/toolchain.py +0 -0
  70. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/__init__.py +0 -0
  71. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/__init__.py +0 -0
  72. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/steering.py +0 -0
  73. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/teammates.py +0 -0
  74. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/workarounds.py +0 -0
  75. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/pack.toml +0 -0
  76. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/__init__.py +0 -0
  77. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/conductor.py +0 -0
  78. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/droid.py +0 -0
  79. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/native.py +0 -0
  80. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/cli.py +0 -0
  81. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/cmd.py +0 -0
  82. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/contexts.py +0 -0
  83. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/__init__.py +0 -0
  84. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/context.py +0 -0
  85. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/decision_writer.py +0 -0
  86. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/lifecycle.py +0 -0
  87. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/logsink.py +0 -0
  88. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/ops.py +0 -0
  89. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/registry.py +0 -0
  90. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/server.py +0 -0
  91. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/transcache.py +0 -0
  92. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/decisions.py +0 -0
  93. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/dispatch.py +0 -0
  94. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/doc_conventions.py +0 -0
  95. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/durable.py +0 -0
  96. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/events.py +0 -0
  97. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/fields.py +0 -0
  98. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/file.py +0 -0
  99. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/heartbeat.py +0 -0
  100. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/helper/__init__.py +0 -0
  101. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/helper/cli.py +0 -0
  102. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/helper/client.py +0 -0
  103. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/hooks/hooks.json +0 -0
  104. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/loader.py +0 -0
  105. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/log.py +0 -0
  106. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/packs/__init__.py +0 -0
  107. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/packs/manager.py +0 -0
  108. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/packs/plugins.py +0 -0
  109. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/__init__.py +0 -0
  110. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/commands.py +0 -0
  111. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/lint.py +0 -0
  112. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/llm.py +0 -0
  113. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/nudge.py +0 -0
  114. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/permissions.py +0 -0
  115. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/provision.py +0 -0
  116. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/rewrite.py +0 -0
  117. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/workflow.py +0 -0
  118. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/prompt.py +0 -0
  119. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/py.typed +0 -0
  120. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/__init__.py +0 -0
  121. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/announce.py +0 -0
  122. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/cli.py +0 -0
  123. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/dashboard.py +0 -0
  124. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/fix.py +0 -0
  125. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/formats.py +0 -0
  126. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/judge.py +0 -0
  127. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/notify.py +0 -0
  128. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/pipeline.py +0 -0
  129. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/prompts.py +0 -0
  130. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/repo.py +0 -0
  131. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/routing.py +0 -0
  132. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/scan.py +0 -0
  133. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/settings.py +0 -0
  134. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/snapshot.py +0 -0
  135. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/status.py +0 -0
  136. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/store.py +0 -0
  137. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/sync.py +0 -0
  138. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/triage.py +0 -0
  139. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/session.py +0 -0
  140. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/settings.py +0 -0
  141. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/signals/__init__.py +0 -0
  142. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  143. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  144. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  145. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  146. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  147. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  148. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  149. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  150. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  151. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  152. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  153. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/state.py +0 -0
  154. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/__init__.py +0 -0
  155. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/ast_grep.py +0 -0
  156. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/matchers.py +0 -0
  157. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/scope.py +0 -0
  158. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/types.py +0 -0
  159. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/tasks.py +0 -0
  160. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  161. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/__init__.py +0 -0
  162. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/helpers.py +0 -0
  163. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/session_cache.py +0 -0
  164. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/types.py +0 -0
  165. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/transcripts.py +0 -0
  166. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/__init__.py +0 -0
  167. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/automode.py +0 -0
  168. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/caching.py +0 -0
  169. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/fs.py +0 -0
  170. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/globbing.py +0 -0
  171. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/http.py +0 -0
  172. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/model_cache.py +0 -0
  173. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/paths.py +0 -0
  174. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/proc.py +0 -0
  175. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/reqenv.py +0 -0
  176. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/scratch.py +0 -0
  177. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/shell.py +0 -0
  178. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/text.py +0 -0
  179. {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/vcs.py +0 -0
  180. {capt_hook-12.2.0 → capt_hook-12.3.0}/hatch_build.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capt-hook
3
- Version: 12.2.0
3
+ Version: 12.3.0
4
4
  Summary: Stop repeating yourself to Claude.
5
5
  Project-URL: Homepage, https://github.com/yasyf/captain-hook
6
6
  Project-URL: Documentation, https://yasyf.github.io/captain-hook/
@@ -140,7 +140,6 @@ def build_request(event: str, root: str, payload_raw: str, *, async_: bool) -> d
140
140
  "async": async_,
141
141
  "root": root,
142
142
  "cwd": os.getcwd(),
143
- "hooks": None,
144
143
  "env": key.request_env(),
145
144
  "payload_raw": payload_raw,
146
145
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "captain-hook",
3
- "version": "12.1.0",
3
+ "version": "12.2.0",
4
4
  "displayName": "Captain Hook",
5
5
  "description": "Skills for the capt-hook declarative hook framework: bootstrap gates and nudges from a repo's conventions, and translate style guides into enforced StyleRules.",
6
6
  "author": {
@@ -46,6 +46,7 @@ EXPORTS: dict[str, str] = {
46
46
  "Redirects": "captain_hook.conditions",
47
47
  "RewritingExistingPlan": "captain_hook.conditions",
48
48
  "ScratchPath": "captain_hook.conditions",
49
+ "UserSaid": "captain_hook.conditions",
49
50
  "workflow_opt_matches": "captain_hook.conditions",
50
51
  "workflow_opt_values": "captain_hook.conditions",
51
52
  "workflow_script_source": "captain_hook.conditions",
@@ -122,6 +123,7 @@ EXPORTS: dict[str, str] = {
122
123
  "Phrase": "captain_hook.signals.nlp",
123
124
  "has_nominal_subject": "captain_hook.signals.nlp",
124
125
  "is_past_predicate": "captain_hook.signals.nlp",
126
+ "subject_kind": "captain_hook.signals.nlp",
125
127
  "HookState": "captain_hook.state",
126
128
  "PrimitiveState": "captain_hook.state",
127
129
  "WorkflowState": "captain_hook.state",
@@ -129,6 +131,7 @@ EXPORTS: dict[str, str] = {
129
131
  "Task": "captain_hook.tasks",
130
132
  "Tasks": "captain_hook.tasks",
131
133
  "Allow": "captain_hook.testing.types",
134
+ "Turn": "captain_hook.turn",
132
135
  "Ask": "captain_hook.testing.types",
133
136
  "Block": "captain_hook.testing.types",
134
137
  "FileFixture": "captain_hook.testing.types",
@@ -167,6 +170,7 @@ EXPORTS: dict[str, str] = {
167
170
  "ToolInput": "captain_hook.types",
168
171
  "TouchedFile": "captain_hook.types",
169
172
  "UsedSkill": "captain_hook.types",
173
+ "UsedTool": "captain_hook.types",
170
174
  "Waiting": "captain_hook.types",
171
175
  "WorkflowScript": "captain_hook.types",
172
176
  "binary_supports": "captain_hook.util.fs",
@@ -42,6 +42,7 @@ from captain_hook.conditions import Headless as Headless
42
42
  from captain_hook.conditions import Redirects as Redirects
43
43
  from captain_hook.conditions import RewritingExistingPlan as RewritingExistingPlan
44
44
  from captain_hook.conditions import ScratchPath as ScratchPath
45
+ from captain_hook.conditions import UserSaid as UserSaid
45
46
  from captain_hook.conditions import workflow_opt_matches as workflow_opt_matches
46
47
  from captain_hook.conditions import workflow_opt_values as workflow_opt_values
47
48
  from captain_hook.conditions import workflow_script_source as workflow_script_source
@@ -118,6 +119,7 @@ from captain_hook.signals.nlp import NlpSignal as NlpSignal
118
119
  from captain_hook.signals.nlp import Phrase as Phrase
119
120
  from captain_hook.signals.nlp import has_nominal_subject as has_nominal_subject
120
121
  from captain_hook.signals.nlp import is_past_predicate as is_past_predicate
122
+ from captain_hook.signals.nlp import subject_kind as subject_kind
121
123
  from captain_hook.state import HookState as HookState
122
124
  from captain_hook.state import PrimitiveState as PrimitiveState
123
125
  from captain_hook.state import WorkflowState as WorkflowState
@@ -133,6 +135,7 @@ from captain_hook.testing.types import Input as Input
133
135
  from captain_hook.testing.types import Rewrite as Rewrite
134
136
  from captain_hook.testing.types import TranscriptFixture as TranscriptFixture
135
137
  from captain_hook.testing.types import Warn as Warn
138
+ from captain_hook.turn import Turn as Turn
136
139
  from captain_hook.types import Action as Action
137
140
  from captain_hook.types import Agent as Agent
138
141
  from captain_hook.types import And as And
@@ -163,6 +166,7 @@ from captain_hook.types import Tool as Tool
163
166
  from captain_hook.types import ToolInput as ToolInput
164
167
  from captain_hook.types import TouchedFile as TouchedFile
165
168
  from captain_hook.types import UsedSkill as UsedSkill
169
+ from captain_hook.types import UsedTool as UsedTool
166
170
  from captain_hook.types import Waiting as Waiting
167
171
  from captain_hook.types import WorkflowScript as WorkflowScript
168
172
  from captain_hook.util.fs import binary_supports as binary_supports
@@ -82,7 +82,7 @@ def is_tombstone(text: str) -> bool:
82
82
  "deprecate",
83
83
  ),
84
84
  tense="completed",
85
- subject="no_nominal",
85
+ subject=("unnamed", "passive"),
86
86
  ),
87
87
  Clause(
88
88
  verb=Phrase("be"), adj=Phrase("previously", "formerly", "originally", "here"), tense="completed"
@@ -33,6 +33,7 @@ from captain_hook.types import (
33
33
  ToolInput,
34
34
  TouchedFile,
35
35
  UsedSkill,
36
+ UsedTool,
36
37
  Waiting,
37
38
  WorkflowScript,
38
39
  )
@@ -44,6 +45,7 @@ if TYPE_CHECKING:
44
45
  from cc_transcript.query import Session
45
46
 
46
47
  from captain_hook.events import BaseHookEvent
48
+ from captain_hook.signals.nlp import Clause
47
49
  from captain_hook.types import HookSpec
48
50
 
49
51
  # Prose and config file extensions that shouldn't, on their own, demand a code-review pass.
@@ -183,13 +185,24 @@ def is_project_file(evt: BaseHookEvent) -> bool:
183
185
 
184
186
 
185
187
  class UserSaid(CustomCondition):
186
- """Matches when the user's messages contain any of the given keywords."""
188
+ """Matches when any user prompt in the session matches one of ``patterns``.
187
189
 
188
- def __init__(self, *keywords: str) -> None:
189
- self.keywords = keywords
190
+ A string pattern is a case-insensitive regex, so plain keywords keep their
191
+ substring behavior; a :class:`~captain_hook.Clause` runs the dependency-clause
192
+ scan against each prompt. To match only the current turn's prompt, use
193
+ ``evt.ctx.turn.matches`` instead.
194
+
195
+ Example:
196
+ >>> UserSaid("just commit", Clause(noun=Phrase("test"), verb=Phrase("skip")))
197
+ """
198
+
199
+ def __init__(self, *patterns: str | Clause) -> None:
200
+ self.patterns = patterns
190
201
 
191
202
  def check(self, evt: BaseHookEvent) -> bool:
192
- return evt.ctx.t.user_said(*self.keywords)
203
+ from captain_hook.signals.nlp import scan_text
204
+
205
+ return any(scan_text(turn.prompt, self.patterns) for turn in evt.ctx.t.turns if turn.prompt)
193
206
 
194
207
 
195
208
  class AllEditsUnder(CustomCondition):
@@ -393,6 +406,10 @@ def check_condition(c: TCondition, evt: BaseHookEvent) -> bool:
393
406
  )
394
407
  case UsedSkill(names, subagents):
395
408
  return has_used_skill(evt.ctx.transcript, names, subagents=subagents)
409
+ case UsedTool(names, subagents, scope):
410
+ return (evt.ctx.turn if scope == "turn" else evt.ctx.transcript).has_tool(
411
+ "|".join(names), subagents=subagents
412
+ )
396
413
  case ReadFile(patterns, subagents):
397
414
  return has_read_glob(evt.ctx.transcript, *patterns, subagents=subagents)
398
415
  case TouchedFile(patterns, subagents):
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
17
17
  from spawnllm import TModel, TSpecialty
18
18
 
19
19
  from captain_hook.settings import HooksSettings
20
+ from captain_hook.signals.nlp import Clause
21
+ from captain_hook.turn import Turn
20
22
 
21
23
 
22
24
  RECENT_WINDOW = 15
@@ -70,15 +72,31 @@ class HookContext:
70
72
  return self.conf
71
73
 
72
74
  @cached_property
73
- def turn(self) -> Session:
74
- """The one-turn view of the current turn (cached)."""
75
- return self.transcript.current_turn
75
+ def turn(self) -> Turn:
76
+ """The one-turn view of the current turn (cached), with prompt matching via ``matches``."""
77
+ from captain_hook.turn import Turn
78
+
79
+ return Turn((current := self.transcript.current_turn).turns, current.path)
76
80
 
77
81
  @cached_property
78
82
  def prior(self) -> Session:
79
83
  """The session window before the current turn's last exchange (cached)."""
80
84
  return self.transcript.prior()
81
85
 
86
+ def nlp(self, text: str, *patterns: str | Clause) -> bool:
87
+ """Whether ``text`` matches any pattern — the escape hatch for matching arbitrary prose.
88
+
89
+ A string pattern is a case-insensitive regex; a :class:`~captain_hook.Clause`
90
+ runs the dependency-clause scan. For the current turn's prompt, prefer
91
+ ``evt.ctx.turn.matches(*patterns)``.
92
+
93
+ Example:
94
+ >>> evt.ctx.nlp(evt.ctx.t.assistant_text(), Clause(noun=Phrase("test"), verb=Phrase("skip")))
95
+ """
96
+ from captain_hook.signals.nlp import scan_text
97
+
98
+ return scan_text(text, patterns)
99
+
82
100
  def transcript_text(self, *, window: int | None = None) -> str:
83
101
  """The transcript rendered turn by turn under the default budget.
84
102
 
@@ -104,7 +104,6 @@ class Request:
104
104
  async_: bool = False
105
105
  root: str | None = None
106
106
  cwd: str | None = None
107
- hooks: str | None = None
108
107
  env: dict[str, str] = field(default_factory=dict)
109
108
  payload_raw: str = ""
110
109
 
@@ -132,26 +131,50 @@ def decode_request(line: bytes) -> Request:
132
131
  data = json.loads(line)
133
132
  except (ValueError, UnicodeDecodeError) as exc:
134
133
  raise ProtocolError(f"malformed request JSON: {exc}") from exc
135
- match data:
136
- case {
137
- "v": int() as v,
138
- "kind": str() as kind,
139
- "client": {"version": str() as cv, "build": str() as cb, "pid": int() as cpid, "ppid": int() as cppid},
140
- } if kind in REQUEST_KINDS:
141
- return Request(
142
- v=v,
143
- kind=kind,
144
- client=ClientInfo(version=cv, build=cb, pid=cpid, ppid=cppid),
145
- event=data.get("event"),
146
- async_=bool(data.get("async", False)),
147
- root=data.get("root"),
148
- cwd=data.get("cwd"),
149
- hooks=data.get("hooks"),
150
- env=data.get("env") or {},
151
- payload_raw=data.get("payload_raw", ""),
152
- )
153
- case _:
154
- raise ProtocolError(f"invalid request shape: {data!r}")
134
+ if type(data) is not dict:
135
+ raise ProtocolError(f"invalid request shape: {data!r}")
136
+ kind = data.get("kind")
137
+ keys = {"v", "kind", "client"}
138
+ if kind == "event":
139
+ keys |= {"event", "async", "root", "cwd", "env", "payload_raw"}
140
+ if kind not in REQUEST_KINDS or set(data) != keys:
141
+ raise ProtocolError(f"invalid request shape: {data!r}")
142
+ client = data["client"]
143
+ if type(client) is not dict or set(client) != {"version", "build", "pid", "ppid"}:
144
+ raise ProtocolError(f"invalid request shape: {data!r}")
145
+ if not (
146
+ type(data["v"]) is int
147
+ and type(client["version"]) is str
148
+ and type(client["build"]) is str
149
+ and type(client["pid"]) is int
150
+ and type(client["ppid"]) is int
151
+ ):
152
+ raise ProtocolError(f"invalid request shape: {data!r}")
153
+ info = ClientInfo(version=client["version"], build=client["build"], pid=client["pid"], ppid=client["ppid"])
154
+ if kind != "event":
155
+ return Request(v=data["v"], kind=kind, client=info)
156
+ env = data["env"]
157
+ if not (
158
+ type(data["event"]) is str
159
+ and type(data["async"]) is bool
160
+ and type(data["root"]) is str
161
+ and type(data["cwd"]) is str
162
+ and type(env) is dict
163
+ and all(type(key) is str and type(value) is str for key, value in env.items())
164
+ and type(data["payload_raw"]) is str
165
+ ):
166
+ raise ProtocolError(f"invalid request shape: {data!r}")
167
+ return Request(
168
+ v=data["v"],
169
+ kind=kind,
170
+ client=info,
171
+ event=data["event"],
172
+ async_=data["async"],
173
+ root=data["root"],
174
+ cwd=data["cwd"],
175
+ env=env,
176
+ payload_raw=data["payload_raw"],
177
+ )
155
178
 
156
179
 
157
180
  def encode_response(response: Response) -> bytes:
@@ -1,13 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import functools
4
+ import re
4
5
  from collections.abc import Sequence
5
6
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Literal
7
+ from typing import TYPE_CHECKING, Literal, get_args
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from spacy.tokens import Doc, Span, Token
10
11
 
12
+ SubjectKind = Literal["unnamed", "passive", "actor"]
13
+
11
14
 
12
15
  @dataclass(frozen=True, slots=True)
13
16
  class PhraseFields:
@@ -96,18 +99,21 @@ class Clause:
96
99
  are *not* past predicates and *not* counterfactual modal-perfects —
97
100
  "will leave"/"leaving"/"leave it"/"will have left" but not
98
101
  "left the workspace", "left to clean up", or "should have left".
99
- subject: ``"no_nominal"`` vetoes verbs with both a substantive active
100
- subject (see ``has_nominal_subject``) and a direct object — "the
101
- parser removed the node" is vetoed while "we removed it", passives,
102
- and objectless elliptical passives ("config moved to settings.py",
103
- "logic migrated to the worker") still match. ``"none"`` vetoes any
104
- verb with a substantive active subject, transitive or not
105
- imperatives and pronoun subjects ("switch back to plan mode",
106
- "we should replan") still match while described behavior
107
- ("the daemon switches to degraded mode") does not.
102
+ subject: The subject shapes the verb may have (see ``subject_kind``), as
103
+ a tuple ``()`` (default) applies no constraint, and a bare string
104
+ means that one shape. ``"unnamed"``: no substantive active subject
105
+ imperatives ("switch back to plan mode"), pronoun subjects
106
+ ("we should replan"), and true passives ("the file was removed").
107
+ ``"passive"``: a substantive subject but no direct object
108
+ elliptical passives ("config moved to settings.py") and intransitive
109
+ actives ("the parser crashed"). ``"actor"``: a substantive subject
110
+ acting on a direct object — described behavior like "the parser
111
+ removed the node". ``("unnamed",)`` matches directives but not
112
+ descriptions; ``("unnamed", "passive")`` also admits statements
113
+ about the thing acted on.
108
114
 
109
115
  Example:
110
- >>> Clause(verb=Phrase("remove", "delete"), tense="completed", subject="no_nominal")
116
+ >>> Clause(verb=Phrase("remove", "delete"), tense="completed", subject=("unnamed", "passive"))
111
117
  """
112
118
 
113
119
  noun: Phrase | None = None
@@ -115,19 +121,23 @@ class Clause:
115
121
  adj: Phrase | None = None
116
122
  negated: bool = False
117
123
  tense: Literal["any", "completed", "prospective"] = "any"
118
- subject: Literal["any", "no_nominal", "none"] = "any"
124
+ subject: SubjectKind | tuple[SubjectKind, ...] = ()
119
125
 
120
126
  def __post_init__(self) -> None:
127
+ if isinstance(self.subject, str):
128
+ object.__setattr__(self, "subject", (self.subject,))
129
+ if invalid := set(self.subject) - set(get_args(SubjectKind)):
130
+ raise ValueError(f"Unknown subject kinds: {sorted(invalid)}")
121
131
  if (anchor := self.noun or self.verb) is None:
122
132
  raise ValueError("Clause needs a noun or verb anchor")
123
- if self.verb is None and (self.tense != "any" or self.subject != "any"):
133
+ if self.verb is None and (self.tense != "any" or self.subject):
124
134
  raise ValueError("Clause tense and subject constraints require a verb")
125
135
  if not (
126
136
  (self.noun and self.verb)
127
137
  or self.adj
128
138
  or self.negated
129
139
  or self.tense != "any"
130
- or self.subject != "any"
140
+ or self.subject
131
141
  or any(" " in lemma for lemma in anchor.lemmas)
132
142
  ):
133
143
  raise ValueError("Clause needs a second constraint or a compound anchor phrase")
@@ -234,14 +244,18 @@ def tense_matches(tense: Literal["any", "completed", "prospective"], tok: Token)
234
244
  return not is_past_predicate(tok) and not is_modal_perfect(tok)
235
245
 
236
246
 
237
- def subject_matches(subject: Literal["any", "no_nominal", "none"], tok: Token) -> bool:
238
- match subject:
239
- case "any":
240
- return True
241
- case "no_nominal":
242
- return not (has_nominal_subject(tok) and any(c.dep_ == "dobj" for c in tok.children))
243
- case "none":
244
- return not has_nominal_subject(tok)
247
+ def subject_kind(tok: Token) -> SubjectKind:
248
+ """The shape of ``tok``'s subject, as :class:`Clause`'s ``subject`` constraint sees it.
249
+
250
+ ``"unnamed"`` — no substantive active subject (see ``has_nominal_subject``);
251
+ ``"actor"`` — a substantive subject plus a direct object; ``"passive"`` — a
252
+ substantive subject without one.
253
+ """
254
+ if not has_nominal_subject(tok):
255
+ return "unnamed"
256
+ if any(c.dep_ == "dobj" for c in tok.children):
257
+ return "actor"
258
+ return "passive"
245
259
 
246
260
 
247
261
  def verb_candidates(clause: Clause, sent: Span) -> list[Token]:
@@ -251,7 +265,7 @@ def verb_candidates(clause: Clause, sent: Span) -> list[Token]:
251
265
  v
252
266
  for v in find_lemma_matches(clause.verb, sent, {"VERB", "AUX"})
253
267
  if tense_matches(clause.tense, v)
254
- and subject_matches(clause.subject, v)
268
+ and (not clause.subject or subject_kind(v) in clause.subject)
255
269
  and not (
256
270
  clause.tense == "prospective"
257
271
  and not clause.negated
@@ -278,3 +292,10 @@ def nlp_scan(clauses: Sequence[Clause], text: str) -> list[str]:
278
292
  if not text.strip():
279
293
  return []
280
294
  return [sent.text.strip() for sent in parse(text).sents if any(match_clause(clause, sent) for clause in clauses)]
295
+
296
+
297
+ def scan_text(text: str, patterns: Sequence[str | Clause]) -> bool:
298
+ clauses = [p for p in patterns if isinstance(p, Clause)]
299
+ return any(isinstance(p, str) and re.search(p, text, re.IGNORECASE) for p in patterns) or bool(
300
+ clauses and nlp_scan(clauses, text)
301
+ )
@@ -22,7 +22,7 @@ from captain_hook import (
22
22
  Allow, And, Agent, Ask, BaseHookEvent, Block, Event, FilePath, FromSubagent,
23
23
  HookResult, InlineTests, Input, Not, Or, Prompt, RanCommand, ReadFile, Rewrite, Runs,
24
24
  Signal, Signals, SkipPermissions, SourceEdits, TestFile, Tool, ToolInput, TouchedFile,
25
- TranscriptFixture, UsedSkill, Warn, WorkflowScript,
25
+ TranscriptFixture, UsedSkill, UsedTool, UserSaid, Warn, WorkflowScript,
26
26
  approve, block_command, deny, gate, hook, lint, llm_approve, llm_gate, llm_nudge, nudge, on,
27
27
  prompt_check, rewrite_command, set_tool_input, warn_command, workflow, Artifact, Step, text_matches,
28
28
  )
@@ -165,6 +165,8 @@ evaluated first.
165
165
  | Event comes from a subagent/teammate | `FromSubagent()` — the payload carries an `agent_id`; matches the ask's *origin*, where `Agent` matches its *type* |
166
166
  | Session launched with bypass available | `SkipPermissions()` — walks to the nearest `claude` ancestor process and matches `--dangerously-skip-permissions` **or** `--allow-dangerously-skip-permissions`; availability counts as consent, whatever the active `permission_mode` |
167
167
  | Skill was invoked | `UsedSkill("codex")` — bare name also matches `plugin:name` |
168
+ | Tool was previously used | `UsedTool("EnterPlanMode", scope="turn")` — exact names (a `\|`-joined string works); `scope="turn"` searches only the current turn |
169
+ | User said something | `UserSaid("just commit")` — case-insensitive regexes and/or `Clause`s over every user prompt |
168
170
  | File was previously read | `ReadFile("TESTING.md")` — fnmatch globs; anchor dirs with `**/` |
169
171
  | Match only test files | `TestFile()` (`**/test_*.py`, `**/*_test.py`, `**/conftest.py`, `**/tests/**/*.py`, `**/*_test.go`, `**/*.test.*`, `**/*.spec.*`) |
170
172
  | Python source edits (skips tests by default, in-repo only) | `SourceEdits(lang="py")`; `lang` also `ts`, `go`, `rs`, ...; `project_only=False` to also match out-of-repo files |
@@ -177,7 +179,7 @@ evaluated first.
177
179
  | Custom logic | implement `CustomCondition` |
178
180
  <!-- /gen:conditions -->
179
181
 
180
- `ReadFile`/`TouchedFile`/`RanCommand`/`UsedSkill` inspect the session transcript — they are
182
+ `ReadFile`/`TouchedFile`/`RanCommand`/`UsedSkill`/`UsedTool` inspect the session transcript — they are
181
183
  how Stop gates know what already happened. Custom conditions are any object with a
182
184
  `check(self, evt: BaseHookEvent) -> bool` method (a Protocol — no inheritance needed):
183
185
 
@@ -50,7 +50,7 @@ All fields are keyword-only — `Block(pattern="...")`, never `Block("...")`.
50
50
 
51
51
  ## TranscriptFixture recipe
52
52
 
53
- Transcript-history conditions (`TouchedFile`, `RanCommand`, `ReadFile`, `UsedSkill`) read
53
+ Transcript-history conditions (`TouchedFile`, `RanCommand`, `ReadFile`, `UsedSkill`, `UsedTool`) read
54
54
  the session transcript, so their tests supply one. The message shape is Claude Code JSONL:
55
55
  `{"type": "assistant", "message": {"content": [<tool_use blocks>]}}`.
56
56
 
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from cc_transcript.query import Session
6
+
7
+ if TYPE_CHECKING:
8
+ from captain_hook.signals.nlp import Clause
9
+
10
+
11
+ class Turn(Session):
12
+ """The current turn as a one-turn :class:`~cc_transcript.query.Session` view.
13
+
14
+ What ``evt.ctx.turn`` returns: the full windowed transcript surface
15
+ (``user_text``, ``has_tool``, ``tool_calls``, …) plus prompt matching, so
16
+ hooks can test the turn's opening prompt without importing the NLP machinery.
17
+ """
18
+
19
+ __slots__ = ()
20
+
21
+ def matches(self, *patterns: str | Clause) -> bool:
22
+ """Whether the turn's opening prompt matches any pattern.
23
+
24
+ A string pattern is a case-insensitive regex; a
25
+ :class:`~captain_hook.Clause` runs the dependency-clause scan.
26
+
27
+ Example:
28
+ >>> evt.ctx.turn.matches(Clause(noun=Phrase("work"), verb=Phrase("stop", "halt")))
29
+ """
30
+ from captain_hook.signals.nlp import scan_text
31
+
32
+ return scan_text(self.user_text, patterns)
@@ -328,6 +328,28 @@ class UsedSkill:
328
328
  object.__setattr__(self, "subagents", subagents)
329
329
 
330
330
 
331
+ @dataclass(frozen=True, slots=True)
332
+ class UsedTool:
333
+ """Transcript-history condition: true when a tool use named one of ``names`` exists.
334
+
335
+ Pass names variadically; a single ``|``-joined string still works for back-compat.
336
+ ``scope="turn"`` restricts the search to the current turn — the idiom for skipping
337
+ a guard once the agent has complied.
338
+
339
+ Example:
340
+ >>> hook(Event.PreToolUse, skip_if=[UsedTool("EnterPlanMode", scope="turn")], message="...", block=True)
341
+ """
342
+
343
+ names: tuple[str, ...]
344
+ subagents: bool = True
345
+ scope: Literal["session", "turn"] = "session"
346
+
347
+ def __init__(self, *names: str, subagents: bool = True, scope: Literal["session", "turn"] = "session") -> None:
348
+ object.__setattr__(self, "names", _split_names(names))
349
+ object.__setattr__(self, "subagents", subagents)
350
+ object.__setattr__(self, "scope", scope)
351
+
352
+
331
353
  @dataclass(frozen=True, slots=True)
332
354
  class ReadFile(PatternsCondition):
333
355
  """Transcript-history condition: true when a Read tool use targeted a matching file.
@@ -676,6 +698,7 @@ TCondition = (
676
698
  | FromSubagent
677
699
  | SkipPermissions
678
700
  | UsedSkill
701
+ | UsedTool
679
702
  | ReadFile
680
703
  | TestFile
681
704
  | SourceEdits
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
3
  # Inert sentinel — the real version is stamped from the release tag (uv version --frozen).
4
- version = "12.2.0"
4
+ version = "12.3.0"
5
5
  description = "Stop repeating yourself to Claude."
6
6
  readme = "README.md"
7
7
  license = "PolyForm-Noncommercial-1.0.0"
File without changes
File without changes
File without changes