capt-hook 4.1.0__tar.gz → 4.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.
- {capt_hook-4.1.0 → capt_hook-4.2.0}/PKG-INFO +1 -1
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/context.py +44 -16
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/session.py +25 -1
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/state.py +7 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/pyproject.toml +1 -1
- {capt_hook-4.1.0 → capt_hook-4.2.0}/LICENSE +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/README.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/__main__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/app.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/cli.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/command.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/conditions.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/decisions.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/events.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/file.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/llm/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/loader.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/log.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/capt-hook.toml +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/packs/steering/steering.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/prompt.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/py.typed +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/store.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/settings.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/style/types.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/tasks.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/tests/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/tests/helpers.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/tests/review_helpers.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/types.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/util/http.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/utils.py +0 -0
|
@@ -35,10 +35,14 @@ if TYPE_CHECKING:
|
|
|
35
35
|
RECENT_WINDOW = 15
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def transcript_window(transcript: bool | int |
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
def transcript_window(transcript: bool | int | Literal["recent", "full"]) -> int | None:
|
|
39
|
+
match transcript:
|
|
40
|
+
case "full":
|
|
41
|
+
return None
|
|
42
|
+
case True | "recent":
|
|
43
|
+
return RECENT_WINDOW
|
|
44
|
+
case int() as events:
|
|
45
|
+
return events
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
class LenientToolUseBlock(ToolUseBlock):
|
|
@@ -195,23 +199,37 @@ class HookContext:
|
|
|
195
199
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
196
200
|
return None
|
|
197
201
|
|
|
198
|
-
def diff(
|
|
199
|
-
|
|
202
|
+
def diff(
|
|
203
|
+
self,
|
|
204
|
+
source: str = "uncommitted",
|
|
205
|
+
*,
|
|
206
|
+
commit: str | None = None,
|
|
207
|
+
scope: str | None = None,
|
|
208
|
+
budget: int = 4000,
|
|
209
|
+
) -> str | None:
|
|
210
|
+
"""A compact diff via ``ccx diff`` when available, else plain ``git``.
|
|
200
211
|
|
|
201
|
-
Prefers cc-context's token-budgeted ``ccx diff`` and
|
|
202
|
-
|
|
212
|
+
Prefers cc-context's token-budgeted ``ccx diff`` and falls back to ``git`` when ``ccx`` is
|
|
213
|
+
absent, fails, or returns a hunkless symbol-outline (no ``@@`` / ``diff --git`` lines, as in
|
|
214
|
+
jj-colocated repos), so a hook gets a real diff in any repo. The git fallback is bounded to
|
|
215
|
+
roughly ``budget`` tokens with a trailing marker when truncated, so a large diff can't blow
|
|
216
|
+
the caller's context.
|
|
203
217
|
|
|
204
218
|
Args:
|
|
205
|
-
source: ``"uncommitted"`` (
|
|
219
|
+
source: ``"uncommitted"`` (the default), ``"staged"``, or any git ref. Ignored when
|
|
220
|
+
``commit`` is set.
|
|
221
|
+
commit: When set, the diff *introduced by* this commit (root-commit-safe), overriding
|
|
222
|
+
``source``; its git fallback is ``git show --stat -p``, so it carries the commit
|
|
223
|
+
header + diffstat ahead of the patch.
|
|
206
224
|
scope: Restrict the diff to this path.
|
|
207
|
-
budget:
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
The rendered diff, or ``None`` when neither ``ccx`` nor ``git`` produces output.
|
|
225
|
+
budget: Token budget for both the ``ccx`` output and the bounded git fallback.
|
|
211
226
|
"""
|
|
212
|
-
|
|
213
|
-
if (out := self.
|
|
227
|
+
target = f"{commit}~1..{commit}" if commit is not None else source
|
|
228
|
+
if (out := self.ccx_diff(target, scope=scope, budget=budget)) is not None:
|
|
214
229
|
return out
|
|
230
|
+
git_scope = ("--", scope) if scope else ()
|
|
231
|
+
if commit is not None:
|
|
232
|
+
return self.bounded_git(budget, "show", "--stat", "-p", commit, *git_scope)
|
|
215
233
|
match source:
|
|
216
234
|
case "uncommitted":
|
|
217
235
|
args = ["diff"]
|
|
@@ -219,7 +237,17 @@ class HookContext:
|
|
|
219
237
|
args = ["diff", "--staged"]
|
|
220
238
|
case ref:
|
|
221
239
|
args = ["diff", ref]
|
|
222
|
-
return self.
|
|
240
|
+
return self.bounded_git(budget, *args, *git_scope)
|
|
241
|
+
|
|
242
|
+
def ccx_diff(self, target: str, *, scope: str | None, budget: int) -> str | None:
|
|
243
|
+
cmd = ["ccx", "diff", target, "--budget", str(budget), *(("--scope", scope) if scope else ())]
|
|
244
|
+
out = self.call_cli(cmd, throw=False)
|
|
245
|
+
return out if out is not None and ("@@" in out or "diff --git" in out) else None
|
|
246
|
+
|
|
247
|
+
def bounded_git(self, budget: int, *args: str) -> str | None:
|
|
248
|
+
if (out := self.git(*args)) is None or len(out) <= (limit := budget * 4):
|
|
249
|
+
return out
|
|
250
|
+
return out[:limit].rstrip() + f"\n... [diff truncated to ~{budget} tokens] ..."
|
|
223
251
|
|
|
224
252
|
@cached_property
|
|
225
253
|
def changed_paths(self) -> frozenset[Path] | None:
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
import shutil
|
|
6
6
|
import tempfile
|
|
7
7
|
import time
|
|
8
|
-
from collections.abc import Sequence
|
|
8
|
+
from collections.abc import Iterable, Sequence
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import ClassVar, Generic, TypeVar, overload
|
|
11
11
|
|
|
@@ -119,6 +119,30 @@ class SessionStore:
|
|
|
119
119
|
"""
|
|
120
120
|
return self[model].get(model())
|
|
121
121
|
|
|
122
|
+
def once(self, key: str, *, scope: str | None = None) -> bool:
|
|
123
|
+
"""Return ``True`` the first time ``(scope, key)`` is seen this session, ``False`` thereafter.
|
|
124
|
+
|
|
125
|
+
Keyed, scoped dedup for hook authors — the single-key case of :meth:`unseen`.
|
|
126
|
+
"""
|
|
127
|
+
return bool(self.unseen([key], scope=scope))
|
|
128
|
+
|
|
129
|
+
def unseen(self, keys: Iterable[str], *, scope: str | None = None) -> list[str]:
|
|
130
|
+
"""Return the first-sight ``keys`` under ``scope``, recording the whole fresh subset in one write.
|
|
131
|
+
|
|
132
|
+
De-duplicates within the batch (order-preserving) and marks every returned key before any
|
|
133
|
+
downstream filtering, so a batch is never partially recorded; no write when nothing is fresh.
|
|
134
|
+
``scope`` namespaces independent call sites on the shared session store.
|
|
135
|
+
"""
|
|
136
|
+
from captain_hook.state import SeenKeys
|
|
137
|
+
|
|
138
|
+
blob = self.load(SeenKeys)
|
|
139
|
+
seen = blob.seen.setdefault(scope or "", [])
|
|
140
|
+
if not (fresh := [key for key in dict.fromkeys(keys) if key not in seen]):
|
|
141
|
+
return []
|
|
142
|
+
seen.extend(fresh)
|
|
143
|
+
self[SeenKeys].set(blob)
|
|
144
|
+
return fresh
|
|
145
|
+
|
|
122
146
|
@classmethod
|
|
123
147
|
def track(cls, model: type[BaseModel]) -> None:
|
|
124
148
|
"""Register ``model`` so it appears in ``tracked_models()`` and ``tracked_paths()``."""
|
|
@@ -227,5 +227,12 @@ def workflow_state(name: str) -> Callable[[type[T]], type[T]]:
|
|
|
227
227
|
return wrap
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
class SeenKeys(BaseModel):
|
|
231
|
+
"""Session-scoped record of keys observed by ``SessionStore.once``/``unseen``, namespaced by scope."""
|
|
232
|
+
|
|
233
|
+
seen: dict[str, list[str]] = Field(default_factory=dict)
|
|
234
|
+
|
|
235
|
+
|
|
230
236
|
SessionStore.track(HookState)
|
|
231
237
|
SessionStore.track(PrimitiveState)
|
|
238
|
+
SessionStore.track(SeenKeys)
|
|
@@ -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 = "4.
|
|
4
|
+
version = "4.2.0"
|
|
5
5
|
description = "Declarative hook framework for Claude Code"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md
RENAMED
|
File without changes
|
{capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md
RENAMED
|
File without changes
|
{capt_hook-4.1.0 → capt_hook-4.2.0}/captain_hook/skills/scanning-sessions/references/review-cli.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|