capt-hook 9.2.1__tar.gz → 9.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 (134) hide show
  1. {capt_hook-9.2.1 → capt_hook-9.4.0}/PKG-INFO +1 -1
  2. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/app.py +3 -4
  3. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/cli.py +12 -1
  4. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/hooks/hooks.json +26 -13
  5. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/loader.py +24 -2
  6. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/once.py +2 -1
  7. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/tombstones.py +4 -2
  8. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/manager.py +9 -40
  9. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/steering/capt-hook.toml +1 -1
  10. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/steering/steering.py +309 -9
  11. capt_hook-9.4.0/captain_hook/review/announce.py +127 -0
  12. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/cli.py +15 -5
  13. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/dashboard.py +53 -10
  14. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/fix.py +138 -37
  15. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/judge.py +12 -3
  16. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/pipeline.py +146 -18
  17. capt_hook-9.4.0/captain_hook/review/routing.py +108 -0
  18. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/scan.py +92 -12
  19. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/settings.py +4 -1
  20. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/store.py +616 -49
  21. capt_hook-9.4.0/captain_hook/review/sync.py +185 -0
  22. capt_hook-9.4.0/captain_hook/review/triage.py +114 -0
  23. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/signals/nlp.py +72 -14
  24. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/SKILL.md +8 -0
  25. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/scanning-sessions/SKILL.md +63 -20
  26. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +56 -3
  27. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +47 -13
  28. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/state.py +15 -2
  29. {capt_hook-9.2.1 → capt_hook-9.4.0}/pyproject.toml +1 -1
  30. capt_hook-9.2.1/captain_hook/review/sync.py +0 -103
  31. {capt_hook-9.2.1 → capt_hook-9.4.0}/LICENSE +0 -0
  32. {capt_hook-9.2.1 → capt_hook-9.4.0}/README.md +0 -0
  33. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  34. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/__init__.py +0 -0
  35. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/__main__.py +0 -0
  36. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/ast_grep.py +0 -0
  37. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/classifiers/__init__.py +0 -0
  38. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/classifiers/conductor.py +0 -0
  39. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/classifiers/droid.py +0 -0
  40. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/classifiers/native.py +0 -0
  41. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/conditions.py +0 -0
  42. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/context.py +0 -0
  43. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/contexts.py +0 -0
  44. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/decisions.py +0 -0
  45. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/dispatch.py +0 -0
  46. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/durable.py +0 -0
  47. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/events.py +0 -0
  48. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/fields.py +0 -0
  49. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/file.py +0 -0
  50. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/log.py +0 -0
  51. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/__init__.py +0 -0
  52. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/fixes/__init__.py +0 -0
  53. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
  54. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
  55. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/__init__.py +0 -0
  56. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/_lib.py +0 -0
  57. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/capt-hook.toml +0 -0
  58. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/commands.py +0 -0
  59. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/comments.py +0 -0
  60. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/detours.py +0 -0
  61. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/docs.py +0 -0
  62. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/models.py +0 -0
  63. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/plans.py +0 -0
  64. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/fragments/deliverable_rubric.md +0 -0
  65. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/fragments/workflow_script_header.md +0 -0
  66. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/implementation_spawn_nudge.md +0 -0
  67. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/inline_edit_nudge.md +0 -0
  68. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/prose_spawn_gate.md +0 -0
  69. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/prose_workflow_nudge.md +0 -0
  70. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/review_routing_spawn_nudge.md +0 -0
  71. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/review_routing_workflow_nudge.md +0 -0
  72. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/writing_docs_spawn_nudge.md +0 -0
  73. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/writing_docs_workflow_nudge.md +0 -0
  74. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/prompts.py +0 -0
  75. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/review.py +0 -0
  76. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/general/tasks.py +0 -0
  77. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/go/__init__.py +0 -0
  78. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/go/capt-hook.toml +0 -0
  79. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/go/testing.py +0 -0
  80. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/go/toolchain.py +0 -0
  81. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/python/__init__.py +0 -0
  82. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  83. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/python/style.py +0 -0
  84. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/python/testing.py +0 -0
  85. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/python/toolchain.py +0 -0
  86. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/steering/__init__.py +0 -0
  87. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/packs/steering/teammates.py +0 -0
  88. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/__init__.py +0 -0
  89. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/commands.py +0 -0
  90. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/lint.py +0 -0
  91. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/llm.py +0 -0
  92. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/nudge.py +0 -0
  93. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/permissions.py +0 -0
  94. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/rewrite.py +0 -0
  95. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/primitives/workflow.py +0 -0
  96. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/prompt.py +0 -0
  97. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/py.typed +0 -0
  98. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/__init__.py +0 -0
  99. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/formats.py +0 -0
  100. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/prompts.py +0 -0
  101. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/review/repo.py +0 -0
  102. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/session.py +0 -0
  103. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/settings.py +0 -0
  104. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/signals/__init__.py +0 -0
  105. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  106. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  107. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  108. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  109. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  110. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  111. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  112. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  113. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  114. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/style/__init__.py +0 -0
  115. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/style/ast_grep.py +0 -0
  116. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/style/matchers.py +0 -0
  117. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/style/scope.py +0 -0
  118. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/style/types.py +0 -0
  119. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/tasks.py +0 -0
  120. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  121. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/testing/__init__.py +0 -0
  122. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/testing/helpers.py +0 -0
  123. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/testing/session_cache.py +0 -0
  124. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/testing/types.py +0 -0
  125. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/transcripts.py +0 -0
  126. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/types.py +0 -0
  127. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/__init__.py +0 -0
  128. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/automode.py +0 -0
  129. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/fs.py +0 -0
  130. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/http.py +0 -0
  131. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/model_cache.py +0 -0
  132. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/paths.py +0 -0
  133. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/proc.py +0 -0
  134. {capt_hook-9.2.1 → capt_hook-9.4.0}/captain_hook/util/text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capt-hook
3
- Version: 9.2.1
3
+ Version: 9.4.0
4
4
  Summary: Stop repeating yourself to Claude.
5
5
  Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
6
6
  Author: Yasyf Mohamedali
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import TYPE_CHECKING, get_args
9
9
 
10
10
  from captain_hook.conditions import matches_conditions
11
+ from captain_hook.state import caller_file, hook_name
11
12
  from captain_hook.types import (
12
13
  Agent,
13
14
  Command,
@@ -113,7 +114,6 @@ class State:
113
114
  settings: HooksSettings | None = None
114
115
  classifier: UserClassifier | None = None
115
116
  load_errors: list[LoadError] = field(default_factory=list)
116
- counter: int = field(default=0, repr=False)
117
117
 
118
118
 
119
119
  _state = State()
@@ -123,7 +123,6 @@ def reset() -> None:
123
123
  _state.hooks.clear()
124
124
  _state.gitignore_patterns.clear()
125
125
  _state.load_errors.clear()
126
- _state.counter = 0
127
126
  _state.settings = None
128
127
  _state.classifier = None
129
128
 
@@ -161,7 +160,6 @@ def hook(
161
160
  ) -> None:
162
161
  validate_conditions(only_if, "only_if", events)
163
162
  validate_conditions(skip_if, "skip_if", events)
164
- _state.counter += 1
165
163
  _state.hooks.append(
166
164
  RegisteredHook(
167
165
  spec=HookSpec(
@@ -176,7 +174,8 @@ def hook(
176
174
  async_=async_,
177
175
  skip_planning_agents=skip_planning_agents,
178
176
  ),
179
- name=f"declarative_{_state.counter}",
177
+ name=hook_name("hook", None, message),
178
+ source_file=caller_file(),
180
179
  )
181
180
  )
182
181
 
@@ -17,7 +17,14 @@ from loguru import logger
17
17
 
18
18
  from captain_hook.app import _state, load_gitignore, reset
19
19
  from captain_hook.dispatch import dispatch
20
- from captain_hook.loader import CONF_MODULE, discover_hooks, discover_pack, is_skip_marked, register_nlp_provisioning
20
+ from captain_hook.loader import (
21
+ CONF_MODULE,
22
+ discover_hooks,
23
+ discover_pack,
24
+ is_skip_marked,
25
+ register_nlp_provisioning,
26
+ register_pr_announcements,
27
+ )
21
28
  from captain_hook.log import setup_logging
22
29
  from captain_hook.once import claim_once
23
30
  from captain_hook.packs import manager
@@ -54,6 +61,10 @@ class CliState:
54
61
  project_nlp = any(pack_.manifest.nlp for pack_ in resolved)
55
62
  if project_nlp:
56
63
  register_nlp_provisioning()
64
+ # The PR announcer is one shared sync SessionStart hook, registered unconditionally.
65
+ # All gating (spawned run, no repo, no DB, repo not watching) lives in
66
+ # collect_announcements, so registration needs no settings or store inspection here.
67
+ register_pr_announcements()
57
68
  attached = self.attached_packs(resolved, session_dir)
58
69
  for pack_ in attached:
59
70
  discover_pack(pack_.entry.name, pack_.path)
@@ -3,85 +3,98 @@
3
3
  "PreToolUse": [
4
4
  {
5
5
  "hooks": [
6
- { "type": "command", "command": "uvx capt-hook run PreToolUse" }
6
+ { "type": "command", "command": "uvx capt-hook run PreToolUse" },
7
+ { "type": "command", "command": "uvx capt-hook run PreToolUse --async", "async": true }
7
8
  ]
8
9
  }
9
10
  ],
10
11
  "PostToolUse": [
11
12
  {
12
13
  "hooks": [
13
- { "type": "command", "command": "uvx capt-hook run PostToolUse" }
14
+ { "type": "command", "command": "uvx capt-hook run PostToolUse" },
15
+ { "type": "command", "command": "uvx capt-hook run PostToolUse --async", "async": true }
14
16
  ]
15
17
  }
16
18
  ],
17
19
  "PostToolUseFailure": [
18
20
  {
19
21
  "hooks": [
20
- { "type": "command", "command": "uvx capt-hook run PostToolUseFailure" }
22
+ { "type": "command", "command": "uvx capt-hook run PostToolUseFailure" },
23
+ { "type": "command", "command": "uvx capt-hook run PostToolUseFailure --async", "async": true }
21
24
  ]
22
25
  }
23
26
  ],
24
27
  "UserPromptSubmit": [
25
28
  {
26
29
  "hooks": [
27
- { "type": "command", "command": "uvx capt-hook run UserPromptSubmit" }
30
+ { "type": "command", "command": "uvx capt-hook run UserPromptSubmit" },
31
+ { "type": "command", "command": "uvx capt-hook run UserPromptSubmit --async", "async": true }
28
32
  ]
29
33
  }
30
34
  ],
31
35
  "Stop": [
32
36
  {
33
37
  "hooks": [
34
- { "type": "command", "command": "uvx capt-hook run Stop" }
38
+ { "type": "command", "command": "uvx capt-hook run Stop" },
39
+ { "type": "command", "command": "uvx capt-hook run Stop --async", "async": true }
35
40
  ]
36
41
  }
37
42
  ],
38
43
  "SubagentStop": [
39
44
  {
40
45
  "hooks": [
41
- { "type": "command", "command": "uvx capt-hook run SubagentStop" }
46
+ { "type": "command", "command": "uvx capt-hook run SubagentStop" },
47
+ { "type": "command", "command": "uvx capt-hook run SubagentStop --async", "async": true }
42
48
  ]
43
49
  }
44
50
  ],
45
51
  "SubagentStart": [
46
52
  {
47
53
  "hooks": [
48
- { "type": "command", "command": "uvx capt-hook run SubagentStart" }
54
+ { "type": "command", "command": "uvx capt-hook run SubagentStart" },
55
+ { "type": "command", "command": "uvx capt-hook run SubagentStart --async", "async": true }
49
56
  ]
50
57
  }
51
58
  ],
52
59
  "PreCompact": [
53
60
  {
54
61
  "hooks": [
55
- { "type": "command", "command": "uvx capt-hook run PreCompact" }
62
+ { "type": "command", "command": "uvx capt-hook run PreCompact" },
63
+ { "type": "command", "command": "uvx capt-hook run PreCompact --async", "async": true }
56
64
  ]
57
65
  }
58
66
  ],
59
67
  "Notification": [
60
68
  {
61
69
  "hooks": [
62
- { "type": "command", "command": "uvx capt-hook run Notification" }
70
+ { "type": "command", "command": "uvx capt-hook run Notification" },
71
+ { "type": "command", "command": "uvx capt-hook run Notification --async", "async": true }
63
72
  ]
64
73
  }
65
74
  ],
66
75
  "PermissionRequest": [
67
76
  {
68
77
  "hooks": [
69
- { "type": "command", "command": "uvx capt-hook run PermissionRequest" }
78
+ { "type": "command", "command": "uvx capt-hook run PermissionRequest" },
79
+ { "type": "command", "command": "uvx capt-hook run PermissionRequest --async", "async": true }
70
80
  ]
71
81
  }
72
82
  ],
73
83
  "SessionStart": [
74
84
  {
75
85
  "hooks": [
76
- { "type": "command", "command": "uvx capt-hook run SessionStart --async", "async": true }
86
+ { "type": "command", "command": "uvx capt-hook run SessionStart" },
87
+ { "type": "command", "command": "uvx capt-hook run SessionStart --async", "async": true },
88
+ { "type": "command", "command": "uvx capt-hook review run", "async": true }
77
89
  ]
78
90
  }
79
91
  ],
80
92
  "SessionEnd": [
81
93
  {
82
94
  "hooks": [
83
- { "type": "command", "command": "uvx capt-hook review run", "async": true },
84
- { "type": "command", "command": "uvx capt-hook run SessionEnd --async", "async": true }
95
+ { "type": "command", "command": "uvx capt-hook run SessionEnd" },
96
+ { "type": "command", "command": "uvx capt-hook run SessionEnd --async", "async": true },
97
+ { "type": "command", "command": "uvx capt-hook review run", "async": true }
85
98
  ]
86
99
  }
87
100
  ]
@@ -14,13 +14,14 @@ from typing import TYPE_CHECKING
14
14
  from loguru import logger
15
15
 
16
16
  from captain_hook.app import LoadError, _state, on
17
+ from captain_hook.state import PACK_PACKAGE_PREFIX
17
18
  from captain_hook.types import Event
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from captain_hook.events import BaseHookEvent
22
+ from captain_hook.types import HookResult
21
23
 
22
24
  CONF_MODULE = "conf"
23
- PACK_PACKAGE_PREFIX = "captain_hook._packs"
24
25
 
25
26
 
26
27
  def is_test_module(fqn: str) -> bool:
@@ -124,8 +125,29 @@ def register_nlp_provisioning() -> None:
124
125
  ensure_nlp_resources()
125
126
 
126
127
 
128
+ def register_pr_announcements() -> None:
129
+ """Register the shared sync SessionStart hook that surfaces changed PR outcomes to the user.
130
+
131
+ Called once per discovery pass, unconditionally: every gate (spawned run, no repo, no
132
+ database, repo not watched) lives in :func:`~captain_hook.review.announce.collect_announcements`.
133
+ The detached reviewer opens and resolves hook PRs on its own; this hook is how a session
134
+ start tells the user what happened.
135
+ """
136
+
137
+ @on(Event.SessionStart, max_fires=1)
138
+ def announce_pr_status(evt: BaseHookEvent) -> HookResult | None:
139
+ from captain_hook.review.announce import collect_announcements
140
+ from captain_hook.types import Action, HookResult
141
+
142
+ if message := collect_announcements(evt.ctx.project_root):
143
+ return HookResult(action=Action.warn, message=message)
144
+ return None
145
+
146
+
127
147
  def discover_pack(name: str, pack_dir: Path) -> None:
128
- pkg = f"{PACK_PACKAGE_PREFIX}.{re.sub(r'\W', '_', name)}"
148
+ from captain_hook.packs.manager import pack_module_name
149
+
150
+ pkg = f"{PACK_PACKAGE_PREFIX}.{pack_module_name(name)}"
129
151
  ensure_pack_package(PACK_PACKAGE_PREFIX, [])
130
152
  ensure_pack_package(pkg, [str(pack_dir)])
131
153
  for path in sorted(pack_dir.glob("*.py")):
@@ -4,7 +4,8 @@ Claude Code runs the byte-identical ``uvx capt-hook run <Event>`` command once p
4
4
  registering source (project settings plus each plugin), so one event fans out into
5
5
  N sibling processes reading identical stdin within milliseconds of each other. A
6
6
  side-effecting hook then fires N times. The guard lets the first sibling win an
7
- ``O_EXCL`` sentinel keyed by the event and its payload; the rest exit silently.
7
+ ``O_EXCL`` sentinel keyed by the event, its sync/async variant, and its payload; the
8
+ rest exit silently.
8
9
 
9
10
  Best-effort only: no locking beyond the atomic create, no daemon. A TTL bounds how
10
11
  long a claim suppresses siblings (covering uvx's multi-second startup skew) and how
@@ -81,10 +81,12 @@ def is_tombstone(text: str) -> bool:
81
81
  "retire",
82
82
  "deprecate",
83
83
  ),
84
- completed=True,
84
+ tense="completed",
85
85
  subject="no_nominal",
86
86
  ),
87
- Clause(verb=Phrase("be"), adj=Phrase("previously", "formerly", "originally", "here"), completed=True),
87
+ Clause(
88
+ verb=Phrase("be"), adj=Phrase("previously", "formerly", "originally", "here"), tense="completed"
89
+ ),
88
90
  ],
89
91
  text,
90
92
  )
@@ -51,6 +51,11 @@ PACK_NAME_RE = re.compile(r"[a-z][a-z0-9-]*")
51
51
  KEEP_COMMITS = 2
52
52
 
53
53
 
54
+ def pack_module_name(name: str) -> str:
55
+ """The import-safe module component a pack loads under (a pack name's hyphens become underscores)."""
56
+ return re.sub(r"\W", "_", name)
57
+
58
+
54
59
  class PackError(Exception):
55
60
  """A pack source, manifest, or enabled-packs entry was invalid or unresolvable."""
56
61
 
@@ -212,16 +217,6 @@ def read_entries(path: Path) -> list[PackEntry]:
212
217
  return [parse_entry(name, table) for name, table in (tomllib.loads(path.read_text()).get("packs") or {}).items()]
213
218
 
214
219
 
215
- def read_launcher(path: Path) -> str | None:
216
- match tomllib.loads(path.read_text()).get("launcher") if path.exists() else None:
217
- case None:
218
- return None
219
- case str() as launcher:
220
- return launcher
221
- case other:
222
- raise PackError(f"invalid launcher in {path}: expected a string, got {type(other).__name__}")
223
-
224
-
225
220
  def render_entry(entry: PackEntry) -> str:
226
221
  match entry:
227
222
  case BuiltinPack(name=name):
@@ -234,34 +229,8 @@ def render_entry(entry: PackEntry) -> str:
234
229
  return f'[packs.{name}]\nsource = "{source}"\ncommit = "{commit}"\n\n'
235
230
 
236
231
 
237
- def toml_basic_string(value: str) -> str:
238
- """Render ``value`` as a quoted TOML basic string, safe for non-BMP characters.
239
-
240
- ``json.dumps`` escapes chars above the BMP (emoji, rare CJK) as surrogate-pair
241
- ``\\uXXXX`` sequences, which the TOML spec rejects — ``tomllib`` then raises on the
242
- next read. This escapes only backslash, double quote, and the ``U+0000``–``U+001F`` /
243
- ``U+007F`` control chars, passing every other char through as literal UTF-8 so the
244
- value round-trips. For ASCII values without control chars the output is byte-identical
245
- to ``json.dumps``.
246
- """
247
-
248
- def escape(c: str) -> str:
249
- match c:
250
- case "\\":
251
- return "\\\\"
252
- case '"':
253
- return '\\"'
254
- case _ if ord(c) <= 0x1F or ord(c) == 0x7F:
255
- return f"\\u{ord(c):04x}"
256
- case _:
257
- return c
258
-
259
- return '"' + "".join(escape(c) for c in value) + '"'
260
-
261
-
262
- def render_packs_toml(entries: Sequence[PackEntry], launcher: str | None = None) -> str:
263
- head = "" if launcher is None else f"launcher = {toml_basic_string(launcher)}\n\n"
264
- return head + "".join(render_entry(e) for e in sorted(entries, key=lambda e: e.name))
232
+ def render_packs_toml(entries: Sequence[PackEntry]) -> str:
233
+ return "".join(render_entry(e) for e in sorted(entries, key=lambda e: e.name))
265
234
 
266
235
 
267
236
  def atomic_write(path: Path, text: str) -> None:
@@ -284,7 +253,7 @@ def atomic_write(path: Path, text: str) -> None:
284
253
  def upsert_entry(path: Path, entry: PackEntry) -> None:
285
254
  atomic_write(
286
255
  path,
287
- render_packs_toml([*(e for e in read_entries(path) if e.name != entry.name), entry], read_launcher(path)),
256
+ render_packs_toml([*(e for e in read_entries(path) if e.name != entry.name), entry]),
288
257
  )
289
258
 
290
259
 
@@ -292,7 +261,7 @@ def delete_entry(path: Path, name: str) -> None:
292
261
  entries = read_entries(path)
293
262
  if name not in {e.name for e in entries}:
294
263
  raise PackError(f"pack {name!r} is not enabled in {path}")
295
- atomic_write(path, render_packs_toml([e for e in entries if e.name != name], read_launcher(path)))
264
+ atomic_write(path, render_packs_toml([e for e in entries if e.name != name]))
296
265
 
297
266
 
298
267
  def packs_cache_root() -> Path:
@@ -1,5 +1,5 @@
1
1
  name = "steering"
2
- version = "0.6.0"
2
+ version = "0.7.0"
3
3
  description = "Steer the agent's judgment on what's worth fixing: prefer fundamental, first-principles fixes over band-aids, don't dismiss a real pre-existing issue, don't chase trivial type noise, don't defer the requested fix by swapping in docs or a follow-up nobody asked for, and remind a starting teammate to return tight digests instead of flooding the shared context."
4
4
  hooks = "."
5
5
  nlp = true