capt-hook 9.1.0__tar.gz → 9.2.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 (130) hide show
  1. {capt_hook-9.1.0 → capt_hook-9.2.0}/PKG-INFO +1 -1
  2. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/cli.py +1 -1
  3. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/hooks/hooks.json +2 -1
  4. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/once.py +9 -5
  5. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/cli.py +2 -33
  6. {capt_hook-9.1.0 → capt_hook-9.2.0}/pyproject.toml +1 -1
  7. {capt_hook-9.1.0 → capt_hook-9.2.0}/LICENSE +0 -0
  8. {capt_hook-9.1.0 → capt_hook-9.2.0}/README.md +0 -0
  9. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  10. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/__init__.py +0 -0
  11. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/__main__.py +0 -0
  12. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/app.py +0 -0
  13. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/ast_grep.py +0 -0
  14. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/classifiers/__init__.py +0 -0
  15. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/classifiers/conductor.py +0 -0
  16. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/classifiers/droid.py +0 -0
  17. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/classifiers/native.py +0 -0
  18. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/conditions.py +0 -0
  19. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/context.py +0 -0
  20. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/contexts.py +0 -0
  21. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/decisions.py +0 -0
  22. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/dispatch.py +0 -0
  23. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/durable.py +0 -0
  24. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/events.py +0 -0
  25. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/fields.py +0 -0
  26. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/file.py +0 -0
  27. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/loader.py +0 -0
  28. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/log.py +0 -0
  29. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/__init__.py +0 -0
  30. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/fixes/__init__.py +0 -0
  31. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
  32. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
  33. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/__init__.py +0 -0
  34. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/_lib.py +0 -0
  35. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/capt-hook.toml +0 -0
  36. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/commands.py +0 -0
  37. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/comments.py +0 -0
  38. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/detours.py +0 -0
  39. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/docs.py +0 -0
  40. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/models.py +0 -0
  41. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/plans.py +0 -0
  42. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/fragments/deliverable_rubric.md +0 -0
  43. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/fragments/workflow_script_header.md +0 -0
  44. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/implementation_spawn_nudge.md +0 -0
  45. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/inline_edit_nudge.md +0 -0
  46. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/prose_spawn_gate.md +0 -0
  47. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/prose_workflow_nudge.md +0 -0
  48. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/review_routing_spawn_nudge.md +0 -0
  49. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/review_routing_workflow_nudge.md +0 -0
  50. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/writing_docs_spawn_nudge.md +0 -0
  51. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts/models/writing_docs_workflow_nudge.md +0 -0
  52. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/prompts.py +0 -0
  53. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/review.py +0 -0
  54. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/tasks.py +0 -0
  55. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/general/tombstones.py +0 -0
  56. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/go/__init__.py +0 -0
  57. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/go/capt-hook.toml +0 -0
  58. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/go/testing.py +0 -0
  59. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/go/toolchain.py +0 -0
  60. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/manager.py +0 -0
  61. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/python/__init__.py +0 -0
  62. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  63. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/python/style.py +0 -0
  64. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/python/testing.py +0 -0
  65. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/python/toolchain.py +0 -0
  66. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/steering/__init__.py +0 -0
  67. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
  68. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/steering/steering.py +0 -0
  69. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/packs/steering/teammates.py +0 -0
  70. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/__init__.py +0 -0
  71. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/commands.py +0 -0
  72. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/lint.py +0 -0
  73. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/llm.py +0 -0
  74. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/nudge.py +0 -0
  75. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/permissions.py +0 -0
  76. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/rewrite.py +0 -0
  77. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/primitives/workflow.py +0 -0
  78. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/prompt.py +0 -0
  79. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/py.typed +0 -0
  80. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/__init__.py +0 -0
  81. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/dashboard.py +0 -0
  82. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/fix.py +0 -0
  83. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/formats.py +0 -0
  84. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/judge.py +0 -0
  85. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/pipeline.py +0 -0
  86. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/prompts.py +0 -0
  87. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/repo.py +0 -0
  88. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/scan.py +0 -0
  89. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/settings.py +0 -0
  90. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/store.py +0 -0
  91. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/review/sync.py +0 -0
  92. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/session.py +0 -0
  93. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/settings.py +0 -0
  94. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/signals/__init__.py +0 -0
  95. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/signals/nlp.py +0 -0
  96. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  97. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  98. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  99. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  100. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  101. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  102. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  103. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  104. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  105. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  106. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  107. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  108. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  109. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/state.py +0 -0
  110. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/style/__init__.py +0 -0
  111. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/style/ast_grep.py +0 -0
  112. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/style/matchers.py +0 -0
  113. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/style/scope.py +0 -0
  114. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/style/types.py +0 -0
  115. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/tasks.py +0 -0
  116. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  117. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/testing/__init__.py +0 -0
  118. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/testing/helpers.py +0 -0
  119. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/testing/session_cache.py +0 -0
  120. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/testing/types.py +0 -0
  121. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/transcripts.py +0 -0
  122. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/types.py +0 -0
  123. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/util/__init__.py +0 -0
  124. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/util/automode.py +0 -0
  125. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/util/fs.py +0 -0
  126. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/util/http.py +0 -0
  127. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/util/model_cache.py +0 -0
  128. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/util/paths.py +0 -0
  129. {capt_hook-9.1.0 → capt_hook-9.2.0}/captain_hook/util/proc.py +0 -0
  130. {capt_hook-9.1.0 → capt_hook-9.2.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.1.0
3
+ Version: 9.2.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
@@ -209,7 +209,7 @@ def run_event(state: CliState, event_name: str, *, async_: bool = False) -> None
209
209
  # Collapse the N byte-identical siblings Claude Code spawns per event to one
210
210
  # dispatch. Decision-capable events (DECISION_EVENTS) are exempt: swallowing a
211
211
  # sibling there could bypass a gate, which outweighs a duplicated side effect.
212
- if event not in DECISION_EVENTS and not claim_once(event_name, raw_text.encode()):
212
+ if event not in DECISION_EVENTS and not claim_once(event_name, raw_text.encode(), async_=async_):
213
213
  return
214
214
 
215
215
  try:
@@ -80,7 +80,8 @@
80
80
  "SessionEnd": [
81
81
  {
82
82
  "hooks": [
83
- { "type": "command", "command": "uvx capt-hook review run", "async": true }
83
+ { "type": "command", "command": "uvx capt-hook review run", "async": true },
84
+ { "type": "command", "command": "uvx capt-hook run SessionEnd --async", "async": true }
84
85
  ]
85
86
  }
86
87
  ]
@@ -26,12 +26,15 @@ DEFAULT_TTL = 10.0
26
26
  TTL_ENV = "CAPT_HOOK_ONCE_TTL"
27
27
 
28
28
 
29
- def claim_once(event_name: str, payload: bytes) -> bool:
30
- """Claim the one-time dispatch of ``payload`` under ``event_name``.
29
+ def claim_once(event_name: str, payload: bytes, *, async_: bool) -> bool:
30
+ """Claim the one-time dispatch of ``payload`` under ``event_name``'s ``async_`` variant.
31
31
 
32
32
  Returns True when this process won the claim and should dispatch, False when a
33
- still-fresh sibling already claimed it (a duplicate that must exit silently).
34
- ``CAPT_HOOK_ONCE_TTL=0`` disables the guard, so every call wins.
33
+ still-fresh sibling already claimed it (a duplicate that must exit silently). The sync
34
+ and async passes of one event dispatch disjoint hook sets (``dispatch`` filters on
35
+ ``spec.async_``), so ``async_`` is part of the key: each variant claims its own token and
36
+ both run even on byte-identical stdin. ``CAPT_HOOK_ONCE_TTL=0`` disables the guard, so
37
+ every call wins.
35
38
  """
36
39
  ttl = _ttl()
37
40
  if ttl <= 0:
@@ -40,7 +43,8 @@ def claim_once(event_name: str, payload: bytes) -> bool:
40
43
  if sentinel_dir is None:
41
44
  return True
42
45
  _reap(sentinel_dir, ttl)
43
- key = hashlib.sha256(event_name.encode() + b"\0" + payload).hexdigest()
46
+ variant = b"async" if async_ else b"sync"
47
+ key = hashlib.sha256(event_name.encode() + b"\0" + variant + b"\0" + payload).hexdigest()
44
48
  return _try_claim(sentinel_dir / key, ttl)
45
49
 
46
50
 
@@ -8,7 +8,6 @@ machinery lazily once it actually runs, and async store calls bridge with
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import json
12
11
  import os
13
12
  import sys
14
13
  from datetime import UTC, datetime
@@ -19,7 +18,6 @@ import click
19
18
 
20
19
  if TYPE_CHECKING:
21
20
  from collections.abc import Awaitable, Callable
22
- from typing import Any
23
21
 
24
22
  from cc_transcript.corrections import Correction
25
23
  from cc_transcript.judge.similar import KeyOverlap
@@ -31,7 +29,6 @@ if TYPE_CHECKING:
31
29
  from captain_hook.review.store import ReviewStore, ThresholdStatus
32
30
  from captain_hook.review.sync import SyncReport
33
31
 
34
- REVIEW_RUN_COMMAND = "capt-hook review run"
35
32
  STATUS_CHOICES = ("watching", "pr_open", "stale", "accepted", "rejected")
36
33
 
37
34
 
@@ -51,33 +48,6 @@ def resolve_repo(repo_: str | None, root: Path) -> RepoKey:
51
48
  return RepoKey(repo_) if repo_ else current_repo(root)
52
49
 
53
50
 
54
- def group_commands(group: dict[str, Any]) -> list[str]:
55
- entries: list[dict[str, Any]] = group.get("hooks") or []
56
- return [str(entry.get("command") or "") for entry in entries]
57
-
58
-
59
- def review_wired(hooks: dict[str, Any]) -> bool:
60
- groups: list[dict[str, Any]] = hooks.get("SessionEnd") or []
61
- return any(REVIEW_RUN_COMMAND in command for group in groups for command in group_commands(group))
62
-
63
-
64
- def ensure_review_wiring(settings_path: Path) -> bool:
65
- from captain_hook.cli import sibling_settings, write_settings
66
-
67
- existing: dict[str, Any] = json.loads(settings_path.read_text()) if settings_path.exists() else {}
68
- sibling = sibling_settings(settings_path)
69
- sibling_hooks: dict[str, Any] = (json.loads(sibling.read_text()).get("hooks") or {}) if sibling.exists() else {}
70
- hooks: dict[str, Any] = existing.get("hooks") or {}
71
- if review_wired(hooks) or review_wired(sibling_hooks):
72
- return False
73
- group = {"hooks": [{"type": "command", "command": f"uvx {REVIEW_RUN_COMMAND}", "async": True}]}
74
- write_settings(
75
- settings_path,
76
- existing | {"hooks": hooks | {"SessionEnd": [*(hooks.get("SessionEnd") or []), group]}},
77
- )
78
- return True
79
-
80
-
81
51
  def run_store[T](fn: Callable[[ReviewStore], Awaitable[T]]) -> T:
82
52
  """Open the review store, run ``fn`` against it, and bridge with ``asyncio.run`` at the command boundary."""
83
53
  import asyncio
@@ -136,14 +106,13 @@ def spawn(transcript: Path, cwd: str | None) -> None:
136
106
  @review.command()
137
107
  @click.pass_obj
138
108
  def enable(state: CliState) -> None:
139
- """Watch the current repo, register the captain-hook plugin, and wire the SessionEnd hook."""
109
+ """Watch the current repo and register the captain-hook plugin."""
140
110
  from captain_hook.cli import register_marketplace
141
111
 
142
112
  repo = current_repo(state.root)
143
113
  watch_repo(repo)
144
114
  register_marketplace(state.root)
145
- wired = ensure_review_wiring(state.root / ".claude" / "settings.json")
146
- click.echo(f"watching {repo}" + (" (SessionEnd hook wired into .claude/settings.json)" if wired else ""))
115
+ click.echo(f"watching {repo}")
147
116
 
148
117
 
149
118
  @review.command()
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
3
  # Inert sentinel — the real version is stamped from the release tag (uv version --frozen).
4
- version = "9.1.0"
4
+ version = "9.2.0"
5
5
  description = "Stop repeating yourself to Claude."
6
6
  readme = "README.md"
7
7
  license = "PolyForm-Noncommercial-1.0.0"
File without changes
File without changes
File without changes
File without changes