capt-hook 10.2.0__tar.gz → 10.4.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 (176) hide show
  1. {capt_hook-10.2.0 → capt_hook-10.4.0}/PKG-INFO +1 -1
  2. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/.claude-plugin/plugin.json +1 -1
  3. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/ast_grep.py +71 -52
  4. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/cli.py +2 -0
  5. capt_hook-10.4.0/captain_hook/helper/__init__.py +8 -0
  6. capt_hook-10.4.0/captain_hook/helper/cli.py +59 -0
  7. capt_hook-10.4.0/captain_hook/helper/client.py +201 -0
  8. capt_hook-10.4.0/captain_hook/packs/general/capt-hook.toml +6 -0
  9. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/comments.py +236 -26
  10. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/cli.py +50 -9
  11. capt_hook-10.4.0/captain_hook/review/notify.py +116 -0
  12. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/pipeline.py +10 -3
  13. capt_hook-10.4.0/captain_hook/review/snapshot.py +137 -0
  14. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/store.py +50 -16
  15. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +6 -8
  16. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +11 -7
  17. {capt_hook-10.2.0 → capt_hook-10.4.0}/hatch_build.py +94 -4
  18. {capt_hook-10.2.0 → capt_hook-10.4.0}/pyproject.toml +1 -1
  19. capt_hook-10.2.0/captain_hook/packs/general/capt-hook.toml +0 -6
  20. {capt_hook-10.2.0 → capt_hook-10.4.0}/.gitignore +0 -0
  21. {capt_hook-10.2.0 → capt_hook-10.4.0}/LICENSE +0 -0
  22. {capt_hook-10.2.0 → capt_hook-10.4.0}/README.md +0 -0
  23. {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/__init__.py +0 -0
  24. {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/__main__.py +0 -0
  25. {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/client.py +0 -0
  26. {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/key.py +0 -0
  27. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/__init__.py +0 -0
  28. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/__init__.pyi +0 -0
  29. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/__main__.py +0 -0
  30. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/app.py +0 -0
  31. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/__init__.py +0 -0
  32. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/conductor.py +0 -0
  33. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/droid.py +0 -0
  34. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/native.py +0 -0
  35. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/conditions.py +0 -0
  36. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/context.py +0 -0
  37. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/contexts.py +0 -0
  38. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/__init__.py +0 -0
  39. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/context.py +0 -0
  40. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/decision_writer.py +0 -0
  41. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/lifecycle.py +0 -0
  42. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/logsink.py +0 -0
  43. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/ops.py +0 -0
  44. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/protocol.py +0 -0
  45. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/registry.py +0 -0
  46. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/server.py +0 -0
  47. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/transcache.py +0 -0
  48. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/decisions.py +0 -0
  49. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/dispatch.py +0 -0
  50. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/durable.py +0 -0
  51. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/events.py +0 -0
  52. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/fields.py +0 -0
  53. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/file.py +0 -0
  54. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/heartbeat.py +0 -0
  55. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/hooks/hooks.json +0 -0
  56. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/loader.py +0 -0
  57. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/log.py +0 -0
  58. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/__init__.py +0 -0
  59. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/bootstrap.py +0 -0
  60. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/contract.py +0 -0
  61. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/__init__.py +0 -0
  62. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/_lib.py +0 -0
  63. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
  64. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/scratch_writes.py +0 -0
  65. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
  66. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/__init__.py +0 -0
  67. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/_lib.py +0 -0
  68. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/commands.py +0 -0
  69. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/deletions.py +0 -0
  70. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/detours.py +0 -0
  71. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/docs.py +0 -0
  72. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/models.py +0 -0
  73. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/plans.py +0 -0
  74. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/fragments/deliverable_rubric.md +0 -0
  75. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/fragments/workflow_script_header.md +0 -0
  76. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/browser_delegation_nudge.md +0 -0
  77. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/implementation_spawn_nudge.md +0 -0
  78. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/inline_edit_nudge.md +0 -0
  79. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/prose_spawn_gate.md +0 -0
  80. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/prose_workflow_nudge.md +0 -0
  81. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/review_routing_spawn_nudge.md +0 -0
  82. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/review_routing_workflow_nudge.md +0 -0
  83. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/writing_docs_spawn_nudge.md +0 -0
  84. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/writing_docs_workflow_nudge.md +0 -0
  85. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/tools/preload_nudge.md +0 -0
  86. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts.py +0 -0
  87. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/review.py +0 -0
  88. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/tasks.py +0 -0
  89. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/tombstones.py +0 -0
  90. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/tools.py +0 -0
  91. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/__init__.py +0 -0
  92. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/capt-hook.toml +0 -0
  93. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/testing.py +0 -0
  94. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/toolchain.py +0 -0
  95. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/manager.py +0 -0
  96. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/performance/__init__.py +0 -0
  97. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/performance/capt-hook.toml +0 -0
  98. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/performance/pipelining.py +0 -0
  99. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/plugins.py +0 -0
  100. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/__init__.py +0 -0
  101. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  102. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/style.py +0 -0
  103. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/testing.py +0 -0
  104. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/toolchain.py +0 -0
  105. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/scaffold.py +0 -0
  106. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/__init__.py +0 -0
  107. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
  108. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/steering.py +0 -0
  109. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/teammates.py +0 -0
  110. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/__init__.py +0 -0
  111. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/commands.py +0 -0
  112. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/lint.py +0 -0
  113. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/llm.py +0 -0
  114. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/nudge.py +0 -0
  115. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/permissions.py +0 -0
  116. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/rewrite.py +0 -0
  117. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/workflow.py +0 -0
  118. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/prompt.py +0 -0
  119. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/py.typed +0 -0
  120. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/__init__.py +0 -0
  121. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/announce.py +0 -0
  122. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/dashboard.py +0 -0
  123. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/fix.py +0 -0
  124. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/formats.py +0 -0
  125. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/judge.py +0 -0
  126. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/prompts.py +0 -0
  127. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/repo.py +0 -0
  128. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/routing.py +0 -0
  129. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/scan.py +0 -0
  130. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/settings.py +0 -0
  131. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/status.py +0 -0
  132. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/sync.py +0 -0
  133. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/triage.py +0 -0
  134. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/session.py +0 -0
  135. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/settings.py +0 -0
  136. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/signals/__init__.py +0 -0
  137. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/signals/nlp.py +0 -0
  138. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  139. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  140. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  141. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  142. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  143. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  144. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  145. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  146. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  147. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  148. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  149. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/state.py +0 -0
  150. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/__init__.py +0 -0
  151. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/ast_grep.py +0 -0
  152. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/matchers.py +0 -0
  153. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/scope.py +0 -0
  154. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/types.py +0 -0
  155. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/tasks.py +0 -0
  156. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  157. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/templates/pack_hook.py.tmpl +0 -0
  158. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/__init__.py +0 -0
  159. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/helpers.py +0 -0
  160. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/session_cache.py +0 -0
  161. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/types.py +0 -0
  162. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/transcripts.py +0 -0
  163. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/types.py +0 -0
  164. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/__init__.py +0 -0
  165. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/automode.py +0 -0
  166. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/caching.py +0 -0
  167. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/fs.py +0 -0
  168. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/http.py +0 -0
  169. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/model_cache.py +0 -0
  170. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/paths.py +0 -0
  171. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/proc.py +0 -0
  172. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/reqenv.py +0 -0
  173. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/scratch.py +0 -0
  174. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/shell.py +0 -0
  175. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/text.py +0 -0
  176. {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/vcs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capt-hook
3
- Version: 10.2.0
3
+ Version: 10.4.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/
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "captain-hook",
3
- "version": "10.1.0",
3
+ "version": "10.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": {
@@ -18,9 +18,11 @@ import re
18
18
  from collections import Counter
19
19
  from collections.abc import Iterable, Iterator, Set
20
20
  from dataclasses import dataclass
21
- from typing import TYPE_CHECKING
21
+ from typing import TYPE_CHECKING, NamedTuple
22
22
 
23
23
  from captain_hook.langs import COMMENT_TYPES as GENERATED_COMMENT_TYPES
24
+ from captain_hook.langs import DOC_COMMENT_KINDS as GENERATED_DOC_COMMENT_KINDS
25
+ from captain_hook.langs import DOC_SIBLINGS as GENERATED_DOC_SIBLINGS
24
26
  from captain_hook.langs import LANG_GLOBS
25
27
 
26
28
  if TYPE_CHECKING:
@@ -46,39 +48,11 @@ MAX_COMMENT_LINES = 3
46
48
  MAX_COMMENT_CHARS = 200
47
49
  MAX_COMMENT_SCAN_BYTES = 512_000
48
50
 
49
- DOC_PREFIXES: dict[str, tuple[str, ...]] = {
50
- "rs": ("///", "//!", "/**", "/*!"),
51
- "js": ("/**",),
52
- "jsx": ("/**",),
53
- "ts": ("/**",),
54
- "tsx": ("/**",),
55
- "java": ("/**",),
56
- "kotlin": ("/**",),
57
- "swift": ("///", "/**"),
58
- "dart": ("///",),
59
- "cs": ("///",),
60
- "php": ("/**",),
61
- "scala": ("/**",),
62
- }
63
- """Comment prefixes that mark a documentation comment, by language."""
64
-
65
- GO_DOC_SIBLINGS: frozenset[str] = frozenset(
66
- {
67
- "function_declaration",
68
- "method_declaration",
69
- "type_declaration",
70
- "var_declaration",
71
- "const_declaration",
72
- "package_clause",
73
- "const_spec",
74
- "var_spec",
75
- "field_declaration",
76
- "import_declaration",
77
- "method_elem",
78
- }
79
- )
80
- """Kinds a Go run must immediately precede to read as godoc — top-level declarations plus grouped
81
- ``const``/``var`` specs, struct fields, imports, and interface methods (which nest below the file)."""
51
+ DOC_COMMENT_KINDS: frozenset[str] = GENERATED_DOC_COMMENT_KINDS
52
+ """Grammar-defined kinds that natively mark a documentation comment."""
53
+
54
+ DOC_SIBLINGS: dict[str, frozenset[str]] = GENERATED_DOC_SIBLINGS
55
+ """Concrete declaration kinds a comment run may document, by language."""
82
56
 
83
57
 
84
58
  @dataclass(frozen=True, slots=True)
@@ -179,6 +153,16 @@ def introduced_comments(old: str, new: str, lang: str) -> Iterator[Match]:
179
153
  return introduced(comments(old, lang), comments(new, lang))
180
154
 
181
155
 
156
+ class CommentSegment(NamedTuple):
157
+ """Line and character sizes of one alphanumeric-bearing portion of a comment block."""
158
+
159
+ lines: int
160
+ chars: int
161
+
162
+ def over(self, max_lines: int, max_chars: int) -> bool:
163
+ return self.lines > max_lines or self.chars > max_chars
164
+
165
+
182
166
  @dataclass(frozen=True, slots=True)
183
167
  class CommentRun:
184
168
  """A maximal run of adjacent line-leading comments (or one block comment), located by 1-based
@@ -209,7 +193,8 @@ class CommentRun:
209
193
  class CommentBlock:
210
194
  """One or more comment runs a size check treats as a unit: adjacent line-leading runs and the
211
195
  paragraphs they form across blank-only gaps. ``lines``/``chars`` sum the constituent runs (the
212
- blank gaps don't count), and the block is doc only when every run is."""
196
+ blank gaps don't count), the block is doc only when every run is, and doc budgets split its
197
+ opening paragraph from the combined trailing paragraphs without changing the runs."""
213
198
 
214
199
  runs: tuple[CommentRun, ...]
215
200
 
@@ -237,9 +222,40 @@ class CommentBlock:
237
222
  def key(self) -> str:
238
223
  return " ".join("\n".join(t for run in self.runs for t in run.texts).split())
239
224
 
225
+ @property
226
+ def doc_paragraphs(self) -> tuple[CommentSegment, CommentSegment]:
227
+ """Opening and trailing doc portions, split at the first alphanumeric-free row.
228
+
229
+ A bare comment leader, gutter, divider, or source gap between runs is a separator. Decoration
230
+ and separator rows count toward neither portion.
231
+ """
232
+ rows = [row for run in self.runs for text in ("", *run.texts) for row in text.rstrip("\n").split("\n")]
233
+ head: list[str] = []
234
+ tail: list[str] = []
235
+ head_chars = 0
236
+ tail_chars = 0
237
+ parted = False
238
+ for row in rows:
239
+ content = any(c.isalnum() for c in row)
240
+ if not content:
241
+ parted = parted or bool(head)
242
+ if parted:
243
+ tail_chars += len(row)
244
+ if content:
245
+ tail.append(row)
246
+ else:
247
+ head_chars += len(row)
248
+ if content:
249
+ head.append(row)
250
+ return CommentSegment(len(head), head_chars), CommentSegment(len(tail), tail_chars)
251
+
240
252
  @property
241
253
  def too_long(self) -> bool:
242
- return self.lines > MAX_COMMENT_LINES or self.chars > MAX_COMMENT_CHARS
254
+ """Whether the block exceeds its plain or documentation-comment budget."""
255
+ if not self.doc:
256
+ return self.lines > MAX_COMMENT_LINES or self.chars > MAX_COMMENT_CHARS
257
+ head, tail = self.doc_paragraphs
258
+ return head.over(6, 400) or tail.over(MAX_COMMENT_LINES, MAX_COMMENT_CHARS)
243
259
 
244
260
 
245
261
  def line_span(node: SyntaxNode) -> tuple[int, int]:
@@ -262,19 +278,19 @@ def is_line_leading(node: SyntaxNode, src_lines: list[str]) -> bool:
262
278
 
263
279
 
264
280
  def is_doc_run(nodes: list[SyntaxNode], lang: str) -> bool:
265
- """Whether a comment run is a documentation comment (godoc / rustdoc / JSDoc).
281
+ """Whether a comment run is a documentation comment.
266
282
 
267
- Prefix languages (rustdoc ``///``, JSDoc ``/**``) require *every* node to carry a doc marker, so
268
- a marker line followed by plain narrative doesn't poison the run doc. Go has no prefix: a run is
269
- godoc when its next named sibling a declaration, grouped spec, struct field, import, or
270
- interface method — begins on the immediately following line (a blank line breaks the bond).
283
+ Native markers require every node to carry a grammar-defined doc kind, so narrative cannot ride
284
+ one marked line. Otherwise, a run is documentation when its next sibling is a declaration that
285
+ begins on the immediately following line; a blank line breaks the bond.
271
286
  """
272
- if prefixes := DOC_PREFIXES.get(lang):
273
- return all(n.text.startswith(prefixes) for n in nodes)
274
- if lang != "go":
275
- return False
287
+ if all(
288
+ node.kind in DOC_COMMENT_KINDS or any(child.kind() in DOC_COMMENT_KINDS for child in node.raw.children())
289
+ for node in nodes
290
+ ):
291
+ return True
276
292
  last = nodes[-1].raw
277
- if (nxt := last.next()) is None or nxt.kind() not in GO_DOC_SIBLINGS:
293
+ if (nxt := last.next()) is None or nxt.kind() not in DOC_SIBLINGS[lang]:
278
294
  return False
279
295
  return nxt.range().start.line == last.range().end.line + 1
280
296
 
@@ -343,19 +359,22 @@ def touched_comment_blocks(old: str, new: str, lang: str) -> list[CommentBlock]:
343
359
  """Comment blocks of ``new`` this edit created or grew — a multiset diff over :attr:`CommentBlock.key`.
344
360
 
345
361
  An old block exempts one identically-keyed new block (a genuine move stays exempt; a duplicated
346
- copy past the first counts as touched). A too-long new block is exempt only when an old block
347
- with the same key was *also* too long, so a legacy oversized run stays quiet while a reflow that
348
- pushes a previously-fine run over the threshold does not.
362
+ copy past the first counts as touched). An over-budget new block is exempt only when an old block
363
+ with the same key was *also* over budget, so a legacy oversized run stays quiet while a reflow
364
+ that pushes a previously-fine run over its budget does not.
349
365
  """
350
366
  old_blocks = comment_blocks(old, lang)
351
367
  remaining = Counter(block.key for block in old_blocks)
352
- old_too_long = {block.key for block in old_blocks if block.too_long}
368
+ old_too_long = Counter(block.key for block in old_blocks if block.too_long)
353
369
  touched: list[CommentBlock] = []
354
370
  for block in comment_blocks(new, lang):
355
371
  if remaining[block.key] > 0:
356
372
  remaining[block.key] -= 1
357
- if block.too_long and block.key not in old_too_long:
358
- touched.append(block)
373
+ if block.too_long:
374
+ if old_too_long[block.key] > 0:
375
+ old_too_long[block.key] -= 1
376
+ else:
377
+ touched.append(block)
359
378
  else:
360
379
  touched.append(block)
361
380
  return touched
@@ -18,6 +18,7 @@ from loguru import logger
18
18
 
19
19
  from captain_hook.app import AsyncDecisionError, _state, load_gitignore, reset
20
20
  from captain_hook.dispatch import dispatch
21
+ from captain_hook.helper.cli import helper
21
22
  from captain_hook.loader import (
22
23
  CONF_MODULE,
23
24
  discover_hooks,
@@ -875,6 +876,7 @@ def pack_update(state: CliState, name: str | None) -> None:
875
876
 
876
877
 
877
878
  cli.add_command(review)
879
+ cli.add_command(helper)
878
880
 
879
881
 
880
882
  @cli.group(hidden=True)
@@ -0,0 +1,8 @@
1
+ """The Captain Hook desktop helper's Python seam: paths, the socket client, and the CLI.
2
+
3
+ The helper itself is a signed macOS app; this package is the ``capt-hook`` side that
4
+ addresses it — the frozen ``helper.sock`` protocol client (:mod:`captain_hook.helper.client`)
5
+ and the ``capt-hook helper`` command group (:mod:`captain_hook.helper.cli`).
6
+ """
7
+
8
+ from __future__ import annotations
@@ -0,0 +1,59 @@
1
+ """The ``capt-hook helper`` command group — install, ping, and test the desktop helper.
2
+
3
+ These are the user-facing surface for ``Captain Hook.app``: ``install`` installs the cask and
4
+ launches it, ``status`` pings the running helper, and ``notify`` fires a test banner through
5
+ :mod:`captain_hook.helper.client`. Every side effect (brew, ``open``, the socket) runs only
6
+ when the command is invoked, never at import.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import click
12
+
13
+ from captain_hook.helper import client
14
+
15
+ CASK = "yasyf/tap/captain-hook"
16
+ APP_PATH = "/Applications/Captain Hook.app"
17
+
18
+
19
+ @click.group()
20
+ def helper() -> None:
21
+ """Manage the Captain Hook desktop notification helper."""
22
+
23
+
24
+ @helper.command()
25
+ def install() -> None:
26
+ """Install or upgrade the signed helper via Homebrew, then launch it in the background."""
27
+ import subprocess
28
+
29
+ subprocess.run(["brew", "install", "--cask", "--force", CASK], check=True)
30
+ if subprocess.run(["open", "-g", APP_PATH], capture_output=True).returncode != 0:
31
+ subprocess.run(["open", "-g", "-a", client.APP_NAME], check=False)
32
+ click.echo(
33
+ "Captain Hook installed. Grant notification permission under "
34
+ "System Settings > Notifications > Captain Hook, then add the review widget via "
35
+ "Edit Widgets in Notification Center."
36
+ )
37
+
38
+
39
+ @helper.command()
40
+ def status() -> None:
41
+ """Ping the running helper and print its reported version."""
42
+ try:
43
+ reply = client.send({"v": client.PROTOCOL, "op": "ping"})
44
+ except (OSError, ValueError) as exc:
45
+ raise click.ClickException(f"helper not reachable: {exc}") from exc
46
+ click.echo(f"helper v{reply.get('version')} ok={reply.get('ok')}")
47
+
48
+
49
+ @helper.command()
50
+ @click.option("--kind", default="pr_open", help="Notification kind (pr_open/pr_merged/review_failure)")
51
+ @click.option("--title", default="Captain Hook test", help="Banner title")
52
+ @click.option("--subtitle", default=None, help="Banner subtitle")
53
+ @click.option("--body", default="This is a test notification", help="Banner body")
54
+ @click.option("--url", default=None, help="Click-through URL")
55
+ @click.option("--repo", default=None, help="Repo key threading related notifications")
56
+ def notify(kind: str, title: str, subtitle: str | None, body: str | None, url: str | None, repo: str | None) -> None:
57
+ """Send a test notification through the helper, launching it if needed."""
58
+ outcome = client.notify(kind=kind, title=title, subtitle=subtitle, body=body, url=url, repo=repo)
59
+ click.echo(f"lane={outcome.lane} ok={outcome.ok} error={outcome.error}")
@@ -0,0 +1,201 @@
1
+ """The frozen ``~/.capt-hook/helper.sock`` protocol client — the single notify seam.
2
+
3
+ The desktop helper (``Captain Hook.app``) serves a newline-delimited JSON request/response
4
+ socket, one request per connection, LF-terminated, protocol version ``v`` 1 — the framing
5
+ conventions of :mod:`captain_hook.daemon.protocol`. This module is the ``capt-hook`` client:
6
+ it encodes a request (:func:`send`), and :func:`notify` drives the full connect → launch →
7
+ poll loop the review pipeline fires a notification through, modeled on
8
+ :mod:`capt_hook_client.client`'s ``exchange``/connect-then-spawn shape.
9
+
10
+ The paths module lives here too — :func:`helper_dir`, :func:`socket_path`, :func:`status_path`
11
+ are what every other helper-facing module imports. ``CAPT_HOOK_HELPER_DIR`` overrides the
12
+ location for tests only; the sandboxed widget appex always reads the literal ``~/.capt-hook``.
13
+
14
+ :func:`notify` never raises into its caller: it catches only ``OSError`` (connect/timeout),
15
+ ``subprocess.SubprocessError`` (the ``open`` launch), and ``json.JSONDecodeError`` (a garbled
16
+ reply), and returns a typed :class:`NotifyOutcome` — a dropped notification is logged, never
17
+ an exception on the review hot path. There is no ``osascript`` fallback: the only lanes are
18
+ the socket and a logged drop.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import socket
25
+ import subprocess
26
+ import time
27
+ from dataclasses import dataclass
28
+ from enum import StrEnum
29
+ from pathlib import Path
30
+ from typing import TYPE_CHECKING
31
+
32
+ from loguru import logger
33
+
34
+ from captain_hook.util import reqenv
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import Mapping
38
+
39
+ PROTOCOL = 1
40
+ APP_NAME = "Captain Hook"
41
+
42
+ SOCKET_TIMEOUT = 2.0
43
+ OPEN_TIMEOUT = 5.0
44
+ LAUNCH_POLL_INTERVAL = 0.25
45
+ LAUNCH_POLL_BUDGET = 3.0
46
+ RECV_CHUNK = 65536
47
+ LINE_CAP = 65536
48
+
49
+
50
+ class Lane(StrEnum):
51
+ """Which lane delivered (or dropped) a notification.
52
+
53
+ ``socket`` — the helper accepted the request over ``helper.sock``. ``dropped`` — the helper
54
+ was unreachable and could not be launched (or kept refusing), so the notification was logged
55
+ and discarded. The draft's ``osascript`` lane is removed: an unreachable helper drops.
56
+ """
57
+
58
+ socket = "socket"
59
+ dropped = "dropped"
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class NotifyOutcome:
64
+ """The typed result of a :func:`notify` — which lane ran, whether it succeeded, and why not.
65
+
66
+ Attributes:
67
+ lane: The lane that handled the request (:attr:`Lane.socket` or :attr:`Lane.dropped`).
68
+ ok: Whether the helper acknowledged the notification (``ok:true`` on the wire).
69
+ error: The failure reason — the helper's ``error`` field, or a local drop reason — else ``None``.
70
+ """
71
+
72
+ lane: Lane
73
+ ok: bool
74
+ error: str | None
75
+
76
+
77
+ def helper_dir() -> Path:
78
+ """The helper's home — ``~/.capt-hook`` unless ``CAPT_HOOK_HELPER_DIR`` overrides it (tests only)."""
79
+ if override := reqenv.getenv("CAPT_HOOK_HELPER_DIR"):
80
+ return Path(override)
81
+ return Path.home() / ".capt-hook"
82
+
83
+
84
+ def socket_path() -> Path:
85
+ """The helper control socket, ``<helper_dir>/helper.sock``."""
86
+ return helper_dir() / "helper.sock"
87
+
88
+
89
+ def status_path() -> Path:
90
+ """The snapshot the widget reads, ``<helper_dir>/status.json``."""
91
+ return helper_dir() / "status.json"
92
+
93
+
94
+ def encode(request: Mapping[str, object]) -> bytes:
95
+ """Encodes one request to its exact wire bytes: compact JSON, declared key order, LF-terminated."""
96
+ return (json.dumps(request, separators=(",", ":"), ensure_ascii=False) + "\n").encode()
97
+
98
+
99
+ def send(request: Mapping[str, object], *, timeout: float = SOCKET_TIMEOUT) -> dict[str, object]:
100
+ """Sends one request to the helper and returns its decoded reply — one connection, one exchange.
101
+
102
+ Raises:
103
+ OSError: The socket is unreachable, or the deadline expired mid-exchange.
104
+ json.JSONDecodeError: The helper's reply was not valid JSON.
105
+ """
106
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
107
+ sock.settimeout(timeout)
108
+ try:
109
+ sock.connect(str(socket_path()))
110
+ sock.sendall(encode(request))
111
+ line = _read_line(sock)
112
+ finally:
113
+ sock.close()
114
+ return json.loads(line)
115
+
116
+
117
+ def _read_line(sock: socket.socket) -> bytes:
118
+ buf = bytearray()
119
+ while b"\n" not in buf:
120
+ if not (chunk := sock.recv(RECV_CHUNK)):
121
+ raise OSError("helper closed the connection before responding")
122
+ buf.extend(chunk)
123
+ if len(buf) > LINE_CAP:
124
+ raise OSError("helper response exceeded the line cap")
125
+ return bytes(buf)
126
+
127
+
128
+ def notify(
129
+ *,
130
+ title: str,
131
+ kind: str,
132
+ subtitle: str | None = None,
133
+ body: str | None = None,
134
+ url: str | None = None,
135
+ repo: str | None = None,
136
+ ) -> NotifyOutcome:
137
+ """Fires one notification through the helper, launching it if needed — never raises.
138
+
139
+ Tries the socket; on failure launches ``Captain Hook.app`` with ``open -g`` and polls the
140
+ socket for up to :data:`LAUNCH_POLL_BUDGET` seconds; a helper that stays unreachable (or was
141
+ never installed, so ``open`` fails fast) drops with a logged warning.
142
+
143
+ Args:
144
+ title: The banner title (required).
145
+ kind: The notification kind (``pr_open``/``pr_merged``/``review_failure``).
146
+ subtitle: The banner subtitle, omitted from the wire when ``None``.
147
+ body: The banner body, omitted when ``None``.
148
+ url: The click-through URL (http/https), omitted when ``None``.
149
+ repo: The repo key threading related notifications, omitted when ``None``.
150
+ """
151
+ request: dict[str, object] = {"v": PROTOCOL, "op": "notify", "kind": kind, "title": title}
152
+ for field, value in (("subtitle", subtitle), ("body", body), ("url", url), ("repo", repo)):
153
+ if value is not None:
154
+ request[field] = value
155
+ if len(encode(request)) > LINE_CAP:
156
+ logger.warning("capt-hook notify request exceeds the line cap — dropped", kind=kind)
157
+ return NotifyOutcome(Lane.dropped, ok=False, error="request exceeds line cap")
158
+
159
+ if (outcome := _try_socket(request)) is not None:
160
+ return outcome
161
+ if not _launch():
162
+ logger.info("capt-hook helper not reachable and not launchable — notification dropped", kind=kind)
163
+ return NotifyOutcome(Lane.dropped, ok=False, error="helper not installed")
164
+ deadline = time.monotonic() + LAUNCH_POLL_BUDGET
165
+ while time.monotonic() < deadline:
166
+ time.sleep(LAUNCH_POLL_INTERVAL)
167
+ if (outcome := _try_socket(request)) is not None:
168
+ return outcome
169
+ logger.warning("capt-hook helper did not answer after launch — notification dropped", kind=kind)
170
+ return NotifyOutcome(Lane.dropped, ok=False, error="helper unreachable after launch")
171
+
172
+
173
+ def _try_socket(request: Mapping[str, object]) -> NotifyOutcome | None:
174
+ """Returns the socket-lane outcome, or ``None`` when the helper is unreachable (relaunch worth trying)."""
175
+ try:
176
+ reply = send(request)
177
+ except (OSError, json.JSONDecodeError):
178
+ return None
179
+ if not isinstance(reply, dict):
180
+ return NotifyOutcome(Lane.socket, ok=False, error=f"malformed reply: {reply!r}")
181
+ if reply.get("v") != PROTOCOL:
182
+ return NotifyOutcome(Lane.socket, ok=False, error=f"protocol mismatch: helper v={reply.get('v')!r}")
183
+ return NotifyOutcome(Lane.socket, ok=bool(reply.get("ok")), error=_reply_error(reply))
184
+
185
+
186
+ def _reply_error(reply: Mapping[str, object]) -> str | None:
187
+ return str(err) if (err := reply.get("error")) is not None else None
188
+
189
+
190
+ def _launch() -> bool:
191
+ """Launches the helper in the background; returns ``False`` when ``open`` fails (helper not installed)."""
192
+ try:
193
+ subprocess.run(
194
+ ["open", "-g", "-a", APP_NAME],
195
+ check=True,
196
+ capture_output=True,
197
+ timeout=OPEN_TIMEOUT,
198
+ )
199
+ except (OSError, subprocess.SubprocessError):
200
+ return False
201
+ return True
@@ -0,0 +1,6 @@
1
+ [pack]
2
+ name = "general"
3
+ version = "0.28.0"
4
+ description = "Language-agnostic guards: git/jj command safety, doc & prompt nudges, task & plan discipline, model-routing nudges (review, security audit, diagnosis, and bounded decision-light implementation route to gpt-5.6-sol via the codex-wrapper agent; ambiguous/decision-dense/long-run implementation to opus xhigh), a browser-automation delegation nudge for sustained agent-browser/playwright work driven inline on fable, code-review + docs-freshness gates, a detour check-in nudge, blocking verbose-comment enforcement (comment runs over 3 lines / 200 chars denied on the post-edit text; doc comments are grammar-classified — native doc markers or declaration adjacency — and keep only their opening paragraph carved out, under a 6-line / 400-char ceiling; a comment-density warn), a first-turn nudge to batch-load the always-used deferred tools in one ToolSearch call, and delegated-prose nudges that catch doc writing handed to a subagent or workflow without the writing-docs skill, and blocking rm safety (repo-root deletes are denied everywhere, and on the macOS rewrite path so are the filesystem root, home directories, and any directory that contains a git/jj repository; on macOS, outside-repo literal deletes and globs whose at-most-10 matches are each verified are rewritten to trash when available, while oversized or unverifiable globs remain denied everywhere; risky deletes hidden inside `sh`/`bash -c` payloads and `eval` are checked by descending into the nested command; temp/scratch paths exempt)."
5
+ hooks = "."
6
+ nlp = true