capt-hook 12.76.2__tar.gz → 12.77.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 (232) hide show
  1. {capt_hook-12.76.2 → capt_hook-12.77.0}/PKG-INFO +1 -1
  2. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/__init__.py +3 -0
  3. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/__init__.pyi +3 -0
  4. capt_hook-12.77.0/captain_hook/annotations.py +296 -0
  5. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/app.py +13 -0
  6. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/_lib.py +1 -11
  7. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/queue.py +2 -3
  8. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/vcs.py +26 -12
  9. capt_hook-12.77.0/captain_hook/confirm.py +178 -0
  10. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/dispatch.py +8 -4
  11. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/events.py +30 -2
  12. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/helpers.py +1 -0
  13. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/types.py +5 -1
  14. {capt_hook-12.76.2 → capt_hook-12.77.0}/pyproject.toml +1 -1
  15. {capt_hook-12.76.2 → capt_hook-12.77.0}/.gitignore +0 -0
  16. {capt_hook-12.76.2 → capt_hook-12.77.0}/LICENSE +0 -0
  17. {capt_hook-12.76.2 → capt_hook-12.77.0}/README.md +0 -0
  18. {capt_hook-12.76.2 → capt_hook-12.77.0}/capt_hook_client/__init__.py +0 -0
  19. {capt_hook-12.76.2 → capt_hook-12.77.0}/capt_hook_client/__main__.py +0 -0
  20. {capt_hook-12.76.2 → capt_hook-12.77.0}/capt_hook_client/client.py +0 -0
  21. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  22. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/.mcp.json +0 -0
  23. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/__main__.py +0 -0
  24. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/ast_grep.py +0 -0
  25. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/bindings.py +0 -0
  26. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/__init__.py +0 -0
  27. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/__init__.py +0 -0
  28. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/__init__.py +0 -0
  29. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/_lib.py +0 -0
  30. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py +0 -0
  31. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py +0 -0
  32. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/__init__.py +0 -0
  33. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/__init__.py +0 -0
  34. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/_lib.py +0 -0
  35. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/_sessions.py +0 -0
  36. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/commands.py +0 -0
  37. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/comments.py +0 -0
  38. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/deletions.py +0 -0
  39. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/detours.py +0 -0
  40. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/docs.py +0 -0
  41. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/hook_files.py +0 -0
  42. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/models.py +0 -0
  43. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/plain_english.py +0 -0
  44. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/plans.py +0 -0
  45. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/deliverable_rubric.md +0 -0
  46. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/workflow_script_header.md +0 -0
  47. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/browser_delegation_nudge.md +0 -0
  48. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/implementation_spawn_nudge.md +0 -0
  49. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/inline_edit_nudge.md +0 -0
  50. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_spawn_gate.md +0 -0
  51. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_workflow_nudge.md +0 -0
  52. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_spawn_nudge.md +0 -0
  53. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_workflow_nudge.md +0 -0
  54. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_spawn_nudge.md +0 -0
  55. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_workflow_nudge.md +0 -0
  56. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/plain_english_rules.md +0 -0
  57. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/tools/preload_nudge.md +0 -0
  58. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts.py +0 -0
  59. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/questions.py +0 -0
  60. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/review.py +0 -0
  61. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/sessions.py +0 -0
  62. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tasks.py +0 -0
  63. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tombstones.py +0 -0
  64. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tooling.py +0 -0
  65. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tools.py +0 -0
  66. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/pack.toml +0 -0
  67. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/__init__.py +0 -0
  68. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/hooks/__init__.py +0 -0
  69. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/hooks/testing.py +0 -0
  70. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/hooks/toolchain.py +0 -0
  71. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/__init__.py +0 -0
  72. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/gitdir/.graphite_repo_config +0 -0
  73. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/__init__.py +0 -0
  74. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/__init__.py +0 -0
  75. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/hooks/__init__.py +0 -0
  76. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/hooks/filesystem.py +0 -0
  77. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/hooks/pipelining.py +0 -0
  78. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/__init__.py +0 -0
  79. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/__init__.py +0 -0
  80. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/style.py +0 -0
  81. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/testing.py +0 -0
  82. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/toolchain.py +0 -0
  83. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/__init__.py +0 -0
  84. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/__init__.py +0 -0
  85. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/steering.py +0 -0
  86. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/teammates.py +0 -0
  87. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/workarounds.py +0 -0
  88. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/pack.toml +0 -0
  89. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/__init__.py +0 -0
  90. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/conductor.py +0 -0
  91. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/droid.py +0 -0
  92. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/lane.py +0 -0
  93. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/native.py +0 -0
  94. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/cli.py +0 -0
  95. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/cmd.py +0 -0
  96. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/command_schema.py +0 -0
  97. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/command_schemas.py +0 -0
  98. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/conditions.py +0 -0
  99. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/context.py +0 -0
  100. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/contexts.py +0 -0
  101. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/__init__.py +0 -0
  102. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/context.py +0 -0
  103. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/decision_writer.py +0 -0
  104. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/logsink.py +0 -0
  105. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/registry.py +0 -0
  106. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/tree.py +0 -0
  107. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/decisions.py +0 -0
  108. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/desktop/__init__.py +0 -0
  109. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/desktop/cli.py +0 -0
  110. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/desktop/client.py +0 -0
  111. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/doc_conventions.py +0 -0
  112. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/durable.py +0 -0
  113. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/faults.py +0 -0
  114. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/fields.py +0 -0
  115. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/file.py +0 -0
  116. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/guard_literal.py +0 -0
  117. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/guard_literal.sh +0 -0
  118. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/heartbeat.py +0 -0
  119. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/hook_lint.py +0 -0
  120. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/hooks/hooks.json +0 -0
  121. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/loader.py +0 -0
  122. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/log.py +0 -0
  123. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/mcp_server.py +0 -0
  124. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/packs/__init__.py +0 -0
  125. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/packs/manager.py +0 -0
  126. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/packs/plugins.py +0 -0
  127. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/__init__.py +0 -0
  128. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/commands.py +0 -0
  129. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/lint.py +0 -0
  130. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/llm.py +0 -0
  131. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/nudge.py +0 -0
  132. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/permissions.py +0 -0
  133. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/provision.py +0 -0
  134. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/rewrite.py +0 -0
  135. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/workflow.py +0 -0
  136. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/prompt.py +0 -0
  137. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/py.typed +0 -0
  138. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/__init__.py +0 -0
  139. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/announce.py +0 -0
  140. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/cli.py +0 -0
  141. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/dashboard.py +0 -0
  142. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/fix.py +0 -0
  143. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/formats.py +0 -0
  144. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/judge.py +0 -0
  145. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/notify.py +0 -0
  146. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/pipeline.py +0 -0
  147. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/prompts.py +0 -0
  148. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/repo.py +0 -0
  149. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/routing.py +0 -0
  150. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/scan.py +0 -0
  151. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/settings.py +0 -0
  152. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/snapshot.py +0 -0
  153. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/status.py +0 -0
  154. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/store.py +0 -0
  155. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/sync.py +0 -0
  156. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/triage.py +0 -0
  157. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/scripts/install-binary.sh +0 -0
  158. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/scripts/install-mcp.sh +0 -0
  159. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/session.py +0 -0
  160. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/settings.py +0 -0
  161. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/signals/__init__.py +0 -0
  162. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/signals/nlp.py +0 -0
  163. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  164. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  165. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  166. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  167. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  168. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  169. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  170. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  171. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  172. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  173. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  174. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  175. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  176. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/__init__.py +0 -0
  177. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/client.py +0 -0
  178. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/contracts.py +0 -0
  179. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/review.py +0 -0
  180. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schema.py +0 -0
  181. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/config.schema.json +0 -0
  182. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/context.schema.json +0 -0
  183. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/host-request.schema.json +0 -0
  184. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/host-response.schema.json +0 -0
  185. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/request.schema.json +0 -0
  186. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/response.schema.json +0 -0
  187. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/tool_registry.schema.json +0 -0
  188. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/validation.py +0 -0
  189. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/worker.py +0 -0
  190. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/state.py +0 -0
  191. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/__init__.py +0 -0
  192. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/ast_grep.py +0 -0
  193. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/matchers.py +0 -0
  194. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/scope.py +0 -0
  195. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/types.py +0 -0
  196. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/tasks.py +0 -0
  197. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  198. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/__init__.py +0 -0
  199. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/fixtures.py +0 -0
  200. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/session_cache.py +0 -0
  201. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/snapshots.py +0 -0
  202. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/types.py +0 -0
  203. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/transcripts.py +0 -0
  204. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/turn.py +0 -0
  205. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/__init__.py +0 -0
  206. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/cli.py +0 -0
  207. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/settings.py +0 -0
  208. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/updater.py +0 -0
  209. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/__init__.py +0 -0
  210. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/automode.py +0 -0
  211. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/caching.py +0 -0
  212. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/fs.py +0 -0
  213. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/globbing.py +0 -0
  214. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/http.py +0 -0
  215. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/model_cache.py +0 -0
  216. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/paths.py +0 -0
  217. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/payload.py +0 -0
  218. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/proc.py +0 -0
  219. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/reqenv.py +0 -0
  220. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/scratch.py +0 -0
  221. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/shell.py +0 -0
  222. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/text.py +0 -0
  223. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/userpath.py +0 -0
  224. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/vcs.py +0 -0
  225. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/__init__.py +0 -0
  226. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/__main__.py +0 -0
  227. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/fail_open.py +0 -0
  228. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/protocol.py +0 -0
  229. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/runtime.py +0 -0
  230. {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/service.py +0 -0
  231. {capt_hook-12.76.2 → capt_hook-12.77.0}/hatch_build.py +0 -0
  232. {capt_hook-12.76.2 → capt_hook-12.77.0}/internal/wireproto/guard.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: capt-hook
3
- Version: 12.76.2
3
+ Version: 12.77.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/
@@ -30,6 +30,7 @@ EXPORTS: dict[str, str] = {
30
30
  "util": "captain_hook.util",
31
31
  "hook": "captain_hook.app",
32
32
  "on": "captain_hook.app",
33
+ "Annotated": "captain_hook.annotations",
33
34
  "COMMENT_TYPES": "captain_hook.ast_grep",
34
35
  "Edit": "captain_hook.ast_grep",
35
36
  "Match": "captain_hook.ast_grep",
@@ -59,6 +60,8 @@ EXPORTS: dict[str, str] = {
59
60
  "workflow_opt_matches": "captain_hook.conditions",
60
61
  "workflow_opt_values": "captain_hook.conditions",
61
62
  "workflow_script_source": "captain_hook.conditions",
63
+ "Confirm": "captain_hook.confirm",
64
+ "ConfirmVerdict": "captain_hook.confirm",
62
65
  "HookContext": "captain_hook.context",
63
66
  "AfterEdit": "captain_hook.contexts",
64
67
  "BeforeEdit": "captain_hook.contexts",
@@ -24,6 +24,7 @@ from cc_transcript.tools import WriteCall as WriteCall
24
24
  from captain_hook import file as file
25
25
  from captain_hook import style as style
26
26
  from captain_hook import util as util
27
+ from captain_hook.annotations import Annotated as Annotated
27
28
  from captain_hook.app import hook as hook
28
29
  from captain_hook.app import on as on
29
30
  from captain_hook.ast_grep import COMMENT_TYPES as COMMENT_TYPES
@@ -55,6 +56,8 @@ from captain_hook.conditions import UserSaid as UserSaid
55
56
  from captain_hook.conditions import workflow_opt_matches as workflow_opt_matches
56
57
  from captain_hook.conditions import workflow_opt_values as workflow_opt_values
57
58
  from captain_hook.conditions import workflow_script_source as workflow_script_source
59
+ from captain_hook.confirm import Confirm as Confirm
60
+ from captain_hook.confirm import ConfirmVerdict as ConfirmVerdict
58
61
  from captain_hook.context import HookContext as HookContext
59
62
  from captain_hook.contexts import AfterEdit as AfterEdit
60
63
  from captain_hook.contexts import BeforeEdit as BeforeEdit
@@ -0,0 +1,296 @@
1
+ """The ``ccx:`` annotation notation: one escape and intent grammar for every hook to read."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterator, Mapping
7
+ from dataclasses import dataclass, field
8
+ from itertools import chain
9
+ from typing import TYPE_CHECKING, Literal
10
+
11
+ from cc_transcript.command import PAYLOAD_DEPTH_LIMIT
12
+ from cc_transcript.tools import SkillCall, TaskCall
13
+
14
+ from captain_hook.cmd import Cmd
15
+ from captain_hook.snapshots.client import RemoteSession
16
+ from captain_hook.types import CustomCondition
17
+ from captain_hook.util import reqenv
18
+
19
+ if TYPE_CHECKING:
20
+ from captain_hook.cmd import Call
21
+ from captain_hook.events import BaseHookEvent
22
+
23
+ WORD = r"[a-z0-9][a-z0-9:._-]*"
24
+ COMMENT_TOKEN = re.compile(rf"ccx:({WORD})(?:=({WORD}))?")
25
+ DISPATCH_LINE = re.compile(rf"ccx:((?:[ \t]+{WORD}(?:={WORD})?)+)")
26
+ DISPATCH_PAIR = re.compile(rf"({WORD})(?:=({WORD}))?")
27
+ RAW_ENV = "CAPT_HOOK_CCX_RAW"
28
+ TRUTHY = frozenset({"1", "true", "yes"})
29
+ BREAKS = frozenset(" \t\n;&|()<>")
30
+ SHELLS = frozenset({"sh", "bash", "zsh", "dash", "ksh"})
31
+ OPTION_VALUES = frozenset({"-o", "+o", "-O", "+O"})
32
+ FENCE = re.compile(r"(`{3,}|~{3,})")
33
+
34
+ type Annotations = Mapping[str, str | None]
35
+ type Pair = tuple[str, str | None]
36
+
37
+
38
+ def pair(key: str, value: str) -> Pair:
39
+ return key, value or None
40
+
41
+
42
+ @dataclass
43
+ class CommentScanner:
44
+ """Bash's own reading of where comments sit: quoting, line continuations, and every pending heredoc body."""
45
+
46
+ text: str
47
+ at: int = 0
48
+ found: list[str] = field(default_factory=list)
49
+ heredocs: list[tuple[str, bool]] = field(default_factory=list)
50
+
51
+ def peek(self, offset: int = 0) -> str:
52
+ return self.text[self.at + offset : self.at + offset + 1]
53
+
54
+ def scan(self, close: str = "") -> None:
55
+ word_start, depth = True, 0
56
+ while self.at < len(self.text):
57
+ char = self.peek()
58
+ if char == close and depth == 0:
59
+ self.at += 1
60
+ return
61
+ match char:
62
+ case "\\" if self.peek(1) == "\n":
63
+ self.at += 2
64
+ case "\\":
65
+ self.at += 2
66
+ word_start = False
67
+ case "'":
68
+ self.skip_to("'")
69
+ word_start = False
70
+ case '"':
71
+ self.skip_double()
72
+ word_start = False
73
+ case "#" if word_start:
74
+ end = self.text.find("\n", self.at)
75
+ self.found.append(self.text[self.at : end if end >= 0 else None])
76
+ self.at = end if end >= 0 else len(self.text)
77
+ case "\n":
78
+ self.at += 1
79
+ self.read_heredocs()
80
+ word_start = True
81
+ case "$":
82
+ self.expansion()
83
+ word_start = False
84
+ case "`":
85
+ self.at += 1
86
+ self.scan("`")
87
+ word_start = False
88
+ case "<" if self.text.startswith("<<", self.at) and not self.text.startswith("<<<", self.at):
89
+ self.heredoc()
90
+ word_start = False
91
+ case _:
92
+ depth += (char == "(") - (char == ")") if close == ")" else 0
93
+ self.at += 1
94
+ word_start = char in BREAKS
95
+
96
+ def skip_to(self, quote: str) -> None:
97
+ end = self.text.find(quote, self.at + 1)
98
+ self.at = end + 1 if end >= 0 else len(self.text)
99
+
100
+ def skip_double(self) -> None:
101
+ self.at += 1
102
+ while self.at < len(self.text):
103
+ match self.peek():
104
+ case "\\":
105
+ self.at += 2
106
+ case '"':
107
+ self.at += 1
108
+ return
109
+ case "$":
110
+ self.expansion()
111
+ case "`":
112
+ self.at += 1
113
+ self.scan("`")
114
+ case _:
115
+ self.at += 1
116
+
117
+ def expansion(self) -> None:
118
+ match self.peek(1), self.peek(2):
119
+ case "(", "(":
120
+ self.skip_balanced("(", ")")
121
+ case "(", _:
122
+ self.at += 2
123
+ self.scan(")")
124
+ case "{", _:
125
+ self.skip_balanced("{", "}")
126
+ case "'", _:
127
+ self.at += 1
128
+ self.skip_ansi()
129
+ case _:
130
+ self.at += 1
131
+
132
+ def skip_balanced(self, opener: str, closer: str) -> None:
133
+ self.at += 1
134
+ depth = 0
135
+ while self.at < len(self.text):
136
+ char = self.peek()
137
+ self.at += 1
138
+ depth += (char == opener) - (char == closer)
139
+ if depth == 0:
140
+ return
141
+
142
+ def skip_ansi(self) -> None:
143
+ self.at += 1
144
+ while self.at < len(self.text):
145
+ match self.peek():
146
+ case "\\":
147
+ self.at += 2
148
+ case "'":
149
+ self.at += 1
150
+ return
151
+ case _:
152
+ self.at += 1
153
+
154
+ def heredoc(self) -> None:
155
+ self.at += 2
156
+ strip = self.peek() == "-"
157
+ self.at += strip
158
+ while self.peek() in (" ", "\t"):
159
+ self.at += 1
160
+ delimiter: list[str] = []
161
+ while (char := self.peek()) and char not in BREAKS:
162
+ match char:
163
+ case "'" | '"':
164
+ end = self.text.find(char, self.at + 1)
165
+ delimiter.append(self.text[self.at + 1 : end if end >= 0 else None])
166
+ self.at = end + 1 if end >= 0 else len(self.text)
167
+ case "\\":
168
+ delimiter.append(self.peek(1))
169
+ self.at += 2
170
+ case _:
171
+ delimiter.append(char)
172
+ self.at += 1
173
+ self.heredocs.append(("".join(delimiter), strip))
174
+
175
+ def read_heredocs(self) -> None:
176
+ for delimiter, strip in self.heredocs:
177
+ while self.at < len(self.text):
178
+ end = self.text.find("\n", self.at)
179
+ line = self.text[self.at : end if end >= 0 else None]
180
+ self.at = end + 1 if end >= 0 else len(self.text)
181
+ if (line.lstrip("\t") if strip else line) == delimiter:
182
+ break
183
+ self.heredocs.clear()
184
+
185
+
186
+ def payload(call: Call) -> str | None:
187
+ """The shell text ``call`` runs as code: an ``eval``'s joined arguments, or a shell's ``-c`` command string."""
188
+ if call.name == "eval":
189
+ return " ".join(call.args) or None
190
+ if call.name not in SHELLS:
191
+ return None
192
+ args, inline = iter(call.args), False
193
+ for arg in args:
194
+ if arg in OPTION_VALUES:
195
+ next(args, None)
196
+ elif arg.startswith(("-", "+")) and not arg.startswith("--"):
197
+ inline = inline or (arg.startswith("-") and "c" in arg[1:])
198
+ elif not arg.startswith("--"):
199
+ return arg if inline else None
200
+ return None
201
+
202
+
203
+ def shell_comments(text: str, depth: int = 0) -> Iterator[str]:
204
+ """Every real shell comment in ``text``, the code of nested ``sh -c`` and ``eval`` payloads included."""
205
+ (scanner := CommentScanner(text)).scan()
206
+ yield from scanner.found
207
+ if depth >= PAYLOAD_DEPTH_LIMIT or (cmd := Cmd.parse(text)) is None:
208
+ return
209
+ for call in cmd.calls():
210
+ if call.occurrence.host is None and (code := payload(call)) is not None:
211
+ yield from shell_comments(code, depth + 1)
212
+
213
+
214
+ def comment_pairs(text: str) -> Iterator[Pair]:
215
+ for comment in shell_comments(text):
216
+ for word in comment.lstrip("#").split():
217
+ if token := COMMENT_TOKEN.fullmatch(word):
218
+ yield pair(*token.groups(""))
219
+
220
+
221
+ def header_lines(text: str) -> Iterator[str]:
222
+ """The lines of a dispatch prompt that speak for the prompt itself: none inside a fence or pasted content."""
223
+ fence, pasted = "", False
224
+ for line in map(str.strip, text.splitlines()):
225
+ if fence:
226
+ fence = "" if line.startswith(fence) else fence
227
+ elif pasted:
228
+ pasted = "</pasted_content>" not in line
229
+ elif opened := FENCE.match(line):
230
+ fence = opened.group(1)
231
+ elif line.startswith("<pasted_content"):
232
+ pasted = "</pasted_content>" not in line
233
+ else:
234
+ yield line
235
+
236
+
237
+ def dispatch_pairs(text: str) -> Iterator[Pair]:
238
+ for line in header_lines(text):
239
+ if declared := DISPATCH_LINE.fullmatch(line):
240
+ yield from (pair(*found.groups("")) for found in DISPATCH_PAIR.finditer(declared.group(1)))
241
+
242
+
243
+ def env_pairs() -> Iterator[Pair]:
244
+ if (reqenv.getenv(RAW_ENV) or "").strip().lower() in TRUTHY:
245
+ yield "raw", None
246
+
247
+
248
+ def dispatch_text(evt: BaseHookEvent) -> str:
249
+ match evt.input:
250
+ case TaskCall(prompt=prompt):
251
+ return prompt or ""
252
+ case SkillCall(args=args):
253
+ return args or ""
254
+ return ""
255
+
256
+
257
+ def event_annotations(evt: BaseHookEvent) -> Annotations:
258
+ return dict(chain(env_pairs(), comment_pairs(evt.cmd.raw), dispatch_pairs(dispatch_text(evt))))
259
+
260
+
261
+ def dispatch_prompt(evt: BaseHookEvent) -> str:
262
+ t = evt.ctx.t
263
+ if isinstance(t, RemoteSession):
264
+ return next(iter(t.prompts(selection="first", count=1)), "")
265
+ return next((turn.prompt for turn in t.turns if turn.prompt), "")
266
+
267
+
268
+ def session_annotations(evt: BaseHookEvent) -> Annotations:
269
+ """The annotations the session's dispatch prompt — its first prompt, a lane's brief — declares."""
270
+ return dict(dispatch_pairs(dispatch_prompt(evt)))
271
+
272
+
273
+ class Annotated(CustomCondition):
274
+ """Matches when the event carries the ``ccx:`` annotation ``key``, set to ``value`` when one is given.
275
+
276
+ Three carriers feed :attr:`~captain_hook.BaseHookEvent.annotations`: a ``ccx:<key>[=<value>]`` token in a
277
+ real comment of a Bash command (``gt submit # ccx:raw``), a whole ``ccx: <key>[=<value>] ...`` line in an
278
+ Agent or Task prompt or in Skill args, and ``CAPT_HOOK_CCX_RAW=1`` for ``raw``. ``scope="session"`` reads
279
+ only the session's dispatch prompt instead, so a lane briefed with ``ccx: role=fix`` matches on every call and
280
+ a command can never claim the role for itself.
281
+
282
+ Example:
283
+ >>> hook(Event.PreToolUse, only_if=[Runs("gt", "submit")], skip_if=[Annotated("raw")], message="...")
284
+ >>> Annotated("role", "evidence", scope="session")
285
+ """
286
+
287
+ def __init__(self, key: str, value: str | None = None, *, scope: Literal["event", "session"] = "event") -> None:
288
+ self.key = key
289
+ self.value = value
290
+ self.scope = scope
291
+
292
+ def matches(self, annotations: Annotations) -> bool:
293
+ return self.key in annotations and (self.value is None or annotations[self.key] == self.value)
294
+
295
+ def check(self, evt: BaseHookEvent) -> bool:
296
+ return self.matches(session_annotations(evt) if self.scope == "session" else evt.annotations)
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, get_args
12
12
  from captain_hook.conditions import matches_conditions
13
13
  from captain_hook.state import caller_file, hook_name
14
14
  from captain_hook.types import (
15
+ TOOL_EVENTS,
15
16
  CustomCondition,
16
17
  Event,
17
18
  HookSpec,
@@ -24,6 +25,7 @@ from captain_hook.types import (
24
25
  if TYPE_CHECKING:
25
26
  from cc_transcript.activity import UserClassifier
26
27
 
28
+ from captain_hook.confirm import Confirm
27
29
  from captain_hook.events import BaseHookEvent
28
30
  from captain_hook.settings import HooksSettings
29
31
  from captain_hook.types import HookResult
@@ -69,6 +71,14 @@ def reject_async_fail_closed(async_: bool, on_incomplete: str | None) -> None:
69
71
  raise AsyncDecisionError("on_incomplete is invalid with async_=True: an async hook has no call to block.")
70
72
 
71
73
 
74
+ def reject_confirm_misuse(events: Event, block: bool, confirm: Confirm | None) -> None:
75
+ """Reject a ``confirm`` with no block to confirm, or on an event with no tool call for the model to judge."""
76
+ if confirm is not None and not block:
77
+ raise ValueError("confirm needs block=True: it settles a block, and a warn has nothing to settle.")
78
+ if confirm is not None and events & ~TOOL_EVENTS:
79
+ raise ValueError("confirm judges a tool call, so it runs only on tool events.")
80
+
81
+
72
82
  def reject_transcript_events(transcript_events: int | None) -> None:
73
83
  """Reject a declared window the snapshot engine's ``tail`` operation cannot serve."""
74
84
  if transcript_events is not None and not 0 < transcript_events <= MAX_TRANSCRIPT_EVENTS:
@@ -216,6 +226,7 @@ def hook(
216
226
  only_if: Sequence[TCondition] = (),
217
227
  skip_if: Sequence[TCondition] = (),
218
228
  block: bool = False,
229
+ confirm: Confirm | None = None,
219
230
  advisory_on_deny: bool = False,
220
231
  respect_gitignore: bool = True,
221
232
  max_fires: int | None = None,
@@ -227,6 +238,7 @@ def hook(
227
238
  ) -> None:
228
239
  reject_async_decision(events, async_)
229
240
  reject_mandatory_misuse(events, async_, mandatory)
241
+ reject_confirm_misuse(events, block, confirm)
230
242
  reject_transcript_events(transcript_events)
231
243
  validate_conditions(only_if, "only_if", events)
232
244
  validate_conditions(skip_if, "skip_if", events)
@@ -238,6 +250,7 @@ def hook(
238
250
  skip_if=tuple(skip_if),
239
251
  message=message,
240
252
  block=block,
253
+ confirm=confirm,
241
254
  advisory_on_deny=advisory_on_deny,
242
255
  respect_gitignore=respect_gitignore,
243
256
  max_fires=max_fires,
@@ -8,15 +8,13 @@ from pathlib import Path
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from captain_hook import BaseHookEvent, CustomCommandLineCondition, CustomCondition, FileFixture, Input
11
- from captain_hook.util import reqenv
12
- from captain_hook.util.vcs import ccx_raw_marked, graphite_lane, graphite_lane_of_git_dir
11
+ from captain_hook.util.vcs import graphite_lane, graphite_lane_of_git_dir
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from cc_transcript.command import CommandLine
16
15
 
17
16
  from captain_hook.cmd import Call
18
17
 
19
- RAW_ENV = "CAPT_HOOK_CCX_RAW"
20
18
  FIXTURE_GITDIR = Path(__file__).parents[1] / "gitdir"
21
19
 
22
20
  JJ_READS = frozenset(
@@ -167,14 +165,6 @@ class CcxInstalled(CustomCondition):
167
165
  return shutil.which("ccx") is not None
168
166
 
169
167
 
170
- class RawRequested(CustomCommandLineCondition):
171
- """Matches a command that opts out of ccx steering: a ``# ccx:raw`` comment on the line, or
172
- ``CAPT_HOOK_CCX_RAW`` set for the session. Either runs the raw command as written."""
173
-
174
- def check_command_line(self, evt: BaseHookEvent, cl: CommandLine) -> bool:
175
- return bool(reqenv.getenv(RAW_ENV)) or ccx_raw_marked(evt.cmd.raw)
176
-
177
-
178
168
  def git_probe(call: Call, session_cwd: Path | None, *args: str) -> str | None:
179
169
  """Run a read-only ``git`` query against the repository ``call`` targets; ``None`` when git exits non-zero."""
180
170
  cwd, git_dir = git_location(call, session_cwd)
@@ -7,10 +7,9 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  from loguru import logger
9
9
 
10
- from captain_hook import Allow, BaseHookEvent, Event, HookResult, Input, Tool, on
10
+ from captain_hook import Allow, Annotated, BaseHookEvent, Event, HookResult, Input, Tool, on
11
11
  from captain_hook.builtin_packs.graphite.hooks._lib import (
12
12
  CcxInstalled,
13
- RawRequested,
14
13
  git_location,
15
14
  git_probe,
16
15
  graphite_owns,
@@ -228,7 +227,7 @@ def holds(cwd: Path, planned: list[Push]) -> list[Push]:
228
227
  @on(
229
228
  Event.PreToolUse,
230
229
  only_if=[Tool("Bash"), CcxInstalled()],
231
- skip_if=[RawRequested()],
230
+ skip_if=[Annotated("raw")],
232
231
  tests={
233
232
  Input(command="git push", cwd="/"): Allow(),
234
233
  Input(command="ccx vcs push", cwd="/"): Allow(),
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from captain_hook import (
8
8
  Allow,
9
+ Annotated,
9
10
  BaseHookEvent,
10
11
  CommandSchema,
11
12
  CustomCommandLineCondition,
@@ -30,7 +31,6 @@ from captain_hook.builtin_packs.graphite.hooks._lib import (
30
31
  HasFlag,
31
32
  JJReads,
32
33
  PushesTagRef,
33
- RawRequested,
34
34
  force_pushes,
35
35
  git_location,
36
36
  git_probe,
@@ -67,7 +67,7 @@ GH_PR_VIEW = CommandSchema(
67
67
  hook(
68
68
  Event.PreToolUse,
69
69
  only_if=[Tool("Bash"), GraphiteRuns(("jj",))],
70
- skip_if=[JJReads(), RawRequested()],
70
+ skip_if=[JJReads(), Annotated("raw")],
71
71
  message=(
72
72
  "The repository this command targets keeps its stack in Graphite, which jj writes leave stale. "
73
73
  'Run `ccx vcs ship -m "<msg>"` to commit and submit instead.'
@@ -77,6 +77,10 @@ hook(
77
77
  in_graphite_repo("jj log && jj new"): Warn(pattern="Graphite"),
78
78
  in_graphite_repo("jj log"): Allow(),
79
79
  in_graphite_repo("jj new # ccx:raw"): Allow(),
80
+ in_graphite_repo("bash -c 'jj new # ccx:raw'"): Allow(),
81
+ in_graphite_repo("jj new && echo '# ccx:raw'"): Warn(pattern="Graphite"),
82
+ in_graphite_repo("jj new", env={"CAPT_HOOK_CCX_RAW": "0"}): Warn(pattern="Graphite"),
83
+ in_graphite_repo("jj new", env={"CAPT_HOOK_CCX_RAW": "true"}): Allow(),
80
84
  Input(command="jj new", cwd="/"): Allow(),
81
85
  },
82
86
  )
@@ -96,7 +100,7 @@ hook(
96
100
  ("git", "checkout", "-B"),
97
101
  ),
98
102
  ],
99
- skip_if=[HasFlag("--dry-run"), HasFlag("--tags"), PushesTagRef(), RawRequested()],
103
+ skip_if=[HasFlag("--dry-run"), HasFlag("--tags"), PushesTagRef(), Annotated("raw")],
100
104
  message=(
101
105
  "The repository this command targets keeps its stack in Graphite, so raw git commits, branches, and "
102
106
  'pushes leave it stale. Run `ccx vcs ship -m "<msg>"` instead.'
@@ -123,11 +127,21 @@ hook(
123
127
  HasFlag("--dry-run"),
124
128
  HasFlag("--no-push"),
125
129
  HasFlag("--help", "-h"),
130
+ Annotated("role", scope="session"),
126
131
  ],
127
132
  message="Submit a change only after a review pass over its diff. Run `/cc-review:start` first.",
128
133
  tests={
129
134
  in_graphite_repo("gt submit"): Warn(pattern="review pass"),
130
135
  in_graphite_repo("ccx vcs ship -m x"): Warn(pattern="review pass"),
136
+ in_graphite_repo(
137
+ 'ccx vcs ship -m "fix: x" --new-branch=hooks/x', transcript=[T.user("ccx: role=fix\nShip the fix.")]
138
+ ): Allow(),
139
+ in_graphite_repo(
140
+ 'ccx vcs ship -m "fix: x" --new-branch=hooks/x', transcript=[T.user("Give this lane role=fix and ship.")]
141
+ ): Warn(pattern="review pass"),
142
+ in_graphite_repo("gt submit # ccx:role=fix", transcript=[T.user("Ship this change.")]): Warn(
143
+ pattern="review pass"
144
+ ),
131
145
  in_graphite_repo(
132
146
  "gt submit", transcript=[T.assistant(T.tool("Skill", skill="cc-review:start")), T.user("ship it")]
133
147
  ): Allow(),
@@ -175,7 +189,7 @@ hook(
175
189
  Tool("Bash"),
176
190
  GraphiteRuns(("git", "rebase"), ("git", "merge"), ("git", "pull")),
177
191
  ],
178
- skip_if=[HasFlag("--abort", "--continue", "--quit"), RawRequested()],
192
+ skip_if=[HasFlag("--abort", "--continue", "--quit"), Annotated("raw")],
179
193
  message=(
180
194
  "A raw `git rebase`, `merge`, or `pull` leaves Graphite's parent records stale. "
181
195
  "Run `ccx vcs stack rebase` to replay the stack instead."
@@ -303,7 +317,7 @@ def hand_run_force_push(call: Call, evt: BaseHookEvent) -> bool:
303
317
  @on(
304
318
  Event.PreToolUse,
305
319
  only_if=[Tool("Bash"), CcxInstalled()],
306
- skip_if=[RawRequested()],
320
+ skip_if=[Annotated("raw")],
307
321
  tests={
308
322
  Input(command="gt submit", cwd="/"): Allow(),
309
323
  Input(command="gt restack", cwd="/"): Allow(),
@@ -337,7 +351,7 @@ def stack_writes_go_through_ccx(evt: BaseHookEvent) -> HookResult | None:
337
351
  @on(
338
352
  Event.PreToolUse,
339
353
  only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_submit)],
340
- skip_if=[RawRequested()],
354
+ skip_if=[Annotated("raw")],
341
355
  tests={Input(command="gt submit --ai", cwd="/"): Allow(), Input(command="git status", cwd="/"): Allow()},
342
356
  )
343
357
  def gt_submit_through_ccx(evt: BaseHookEvent) -> HookResult | None:
@@ -349,7 +363,7 @@ def gt_submit_through_ccx(evt: BaseHookEvent) -> HookResult | None:
349
363
  @on(
350
364
  Event.PreToolUse,
351
365
  only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_restack)],
352
- skip_if=[RawRequested()],
366
+ skip_if=[Annotated("raw")],
353
367
  tests={Input(command="gt sync", cwd="/"): Allow(), Input(command="gt log", cwd="/"): Allow()},
354
368
  )
355
369
  def gt_restack_through_ccx(evt: BaseHookEvent) -> HookResult | None:
@@ -362,7 +376,7 @@ def gt_restack_through_ccx(evt: BaseHookEvent) -> HookResult | None:
362
376
  @on(
363
377
  Event.PreToolUse,
364
378
  only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_create)],
365
- skip_if=[RawRequested()],
379
+ skip_if=[Annotated("raw")],
366
380
  tests={Input(command="gt create feat -m x", cwd="/"): Allow(), Input(command="gt log", cwd="/"): Allow()},
367
381
  )
368
382
  def gt_create_through_ccx(evt: BaseHookEvent) -> HookResult | None:
@@ -375,7 +389,7 @@ def gt_create_through_ccx(evt: BaseHookEvent) -> HookResult | None:
375
389
  @on(
376
390
  Event.PreToolUse,
377
391
  only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_modify)],
378
- skip_if=[RawRequested()],
392
+ skip_if=[Annotated("raw")],
379
393
  tests={Input(command="gt modify -m x", cwd="/"): Allow(), Input(command="gt log", cwd="/"): Allow()},
380
394
  )
381
395
  def gt_modify_through_ccx(evt: BaseHookEvent) -> HookResult | None:
@@ -387,7 +401,7 @@ def gt_modify_through_ccx(evt: BaseHookEvent) -> HookResult | None:
387
401
  @on(
388
402
  Event.PreToolUse,
389
403
  only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_rebase)],
390
- skip_if=[RawRequested()],
404
+ skip_if=[Annotated("raw")],
391
405
  tests={Input(command="git rebase main", cwd="/"): Allow(), Input(command="git rebase --abort", cwd="/"): Allow()},
392
406
  )
393
407
  def git_rebase_through_ccx(evt: BaseHookEvent) -> HookResult | None:
@@ -400,7 +414,7 @@ def git_rebase_through_ccx(evt: BaseHookEvent) -> HookResult | None:
400
414
  @on(
401
415
  Event.PreToolUse,
402
416
  only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(conflict_rebase_control)],
403
- skip_if=[RawRequested()],
417
+ skip_if=[Annotated("raw")],
404
418
  tests={
405
419
  Input(command="git rebase --continue", cwd="/"): Allow(),
406
420
  Input(command="git rebase main", cwd="/"): Allow(),
@@ -415,7 +429,7 @@ def conflict_workspace_finishes_through_ccx(evt: BaseHookEvent) -> HookResult |
415
429
  @on(
416
430
  Event.PreToolUse,
417
431
  only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_force_push)],
418
- skip_if=[RawRequested()],
432
+ skip_if=[Annotated("raw")],
419
433
  tests={Input(command="git push -f origin feat", cwd="/"): Allow(), Input(command="git push", cwd="/"): Allow()},
420
434
  )
421
435
  def force_push_through_ccx(evt: BaseHookEvent) -> HookResult | None: