capt-hook 3.20.0__tar.gz → 4.0.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-3.20.0 → capt_hook-4.0.0}/PKG-INFO +3 -3
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/context.py +9 -10
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/fix.py +3 -2
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/formats.py +41 -30
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/scan.py +14 -19
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/tests/helpers.py +5 -1
- capt_hook-4.0.0/captain_hook/tests/review_helpers.py +141 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/pyproject.toml +3 -3
- {capt_hook-3.20.0 → capt_hook-4.0.0}/LICENSE +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/README.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/__main__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/app.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/cli.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/command.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/conditions.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/decisions.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/events.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/file.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/llm/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/loader.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/log.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/capt-hook.toml +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/packs/steering/steering.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/prompt.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/py.typed +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/store.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/session.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/settings.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/state.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/style/types.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/tasks.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/tests/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/types.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/util/http.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capt-hook
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Declarative hook framework for Claude Code
|
|
5
5
|
Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
|
|
6
6
|
Author: Yasyf Mohamedali
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
16
16
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
17
|
Classifier: Topic :: Software Development :: Testing
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Dist: cc-transcript>=
|
|
19
|
+
Requires-Dist: cc-transcript>=7,<8
|
|
20
20
|
Requires-Dist: pydantic>=2.0
|
|
21
21
|
Requires-Dist: pydantic-settings>=2.0
|
|
22
22
|
Requires-Dist: tree-sitter>=0.24
|
|
@@ -31,7 +31,7 @@ Requires-Dist: wn>=1.1.0
|
|
|
31
31
|
Requires-Dist: lazy-object-proxy>=1.12.0
|
|
32
32
|
Requires-Dist: filelock>=3
|
|
33
33
|
Requires-Dist: loguru>=0.7.3
|
|
34
|
-
Requires-Dist: spawnllm>=0.
|
|
34
|
+
Requires-Dist: spawnllm>=0.5.0
|
|
35
35
|
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
36
36
|
Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
|
|
37
37
|
Requires-Dist: pyright>=1.1 ; extra == 'dev'
|
|
@@ -15,7 +15,8 @@ from cc_transcript.query import Session
|
|
|
15
15
|
from cc_transcript.render import Budget, render_turn
|
|
16
16
|
from cc_transcript.tools import parse_tool_call
|
|
17
17
|
from pydantic import BaseModel
|
|
18
|
-
from spawnllm import call_sync,
|
|
18
|
+
from spawnllm import call_sync, extract_sync
|
|
19
|
+
from spawnllm.proc import run_cli
|
|
19
20
|
|
|
20
21
|
from captain_hook.classifiers import detect
|
|
21
22
|
from captain_hook.llm import LlmBackends, TModel, TSpecialty
|
|
@@ -214,12 +215,10 @@ class HookContext:
|
|
|
214
215
|
if transcript:
|
|
215
216
|
template = f"{{transcript}}\n\n<task>\n{template}\n</task>"
|
|
216
217
|
prompt = template.format(*args, **kwargs, transcript=self.transcript_block())
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
timeout=timeout,
|
|
225
|
-
)
|
|
218
|
+
backend = LlmBackends.for_specialty(specialty)
|
|
219
|
+
cwd = os.environ.get("CLAUDE_PROJECT_DIR") or os.environ.get("FACTORY_PROJECT_DIR")
|
|
220
|
+
if response_model is not None:
|
|
221
|
+
return extract_sync(
|
|
222
|
+
prompt, response_model, backend=backend, model=model, agent=agent, cwd=cwd, timeout=timeout
|
|
223
|
+
)
|
|
224
|
+
return call_sync(prompt, backend=backend, model=model, agent=agent, cwd=cwd, timeout=timeout)
|
|
@@ -38,8 +38,9 @@ from typing import TYPE_CHECKING, Literal
|
|
|
38
38
|
|
|
39
39
|
from cc_transcript.filterspec import tool_uses
|
|
40
40
|
from cc_transcript.mining.confidence import MEDIUM, VERY_HIGH, CandidateSignal
|
|
41
|
-
from cc_transcript.mining.signals import
|
|
41
|
+
from cc_transcript.mining.signals import MiningSignal
|
|
42
42
|
from cc_transcript.mining.sourcekind import SourceKind
|
|
43
|
+
from cc_transcript.mining.spec import CONFIDENCE_STEP, bump
|
|
43
44
|
from cc_transcript.models import AssistantEvent, OtherEvent, ToolResultBlock, UserEvent
|
|
44
45
|
|
|
45
46
|
if TYPE_CHECKING:
|
|
@@ -211,7 +212,7 @@ def complaint_signal(marker: Marker, turns_back: int) -> CandidateSignal:
|
|
|
211
212
|
VERY_HIGH if marker.strength == "strong" else MEDIUM,
|
|
212
213
|
(f"{marker.strength}_marker", marker.misfire_class),
|
|
213
214
|
)
|
|
214
|
-
return
|
|
215
|
+
return bump(base, CONFIDENCE_STEP, "tight_proximity") if turns_back <= TIGHT_PROXIMITY_TURNS else base
|
|
215
216
|
|
|
216
217
|
|
|
217
218
|
def iter_hook_complaint_signals(events: Sequence[TranscriptEvent], *, decisions: DecisionLog) -> Iterator[MiningSignal]:
|
|
@@ -2,32 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
The generic :class:`ReviewComment`/:class:`ReviewFormat` types and the
|
|
4
4
|
format-dispatch live in :mod:`cc_transcript.mining`; this module supplies
|
|
5
|
-
the reviewer's policy — the three review formats it recognizes — and
|
|
6
|
-
into the
|
|
7
|
-
conventions (inline ``In file:Lx:`` cites,
|
|
8
|
-
workstream headers) first proven in
|
|
9
|
-
cc-pushback specifics.
|
|
5
|
+
the reviewer's policy — the three review formats it recognizes — and assembles them
|
|
6
|
+
into the :class:`~cc_transcript.mining.ReviewSpec` the review-comment detector runs.
|
|
7
|
+
The shapes are shared transcript conventions (inline ``In file:Lx:`` cites,
|
|
8
|
+
conductor finding blocks, conductor workstream headers) first proven in
|
|
9
|
+
cc-pushback's formats module, not cc-pushback specifics.
|
|
10
|
+
|
|
11
|
+
``conductor-finding`` is a Rust-portable :class:`~cc_transcript.mining.RegexReviewFormat`
|
|
12
|
+
(a declarative claim+suggestion group-join); ``superset-inline`` (lookahead) and
|
|
13
|
+
``conductor-workstream`` (multi-pass header + body scan) stay
|
|
14
|
+
:class:`~cc_transcript.mining.CallableReviewFormat` escape hatches, which keep the
|
|
15
|
+
review-comment detector in Python for those formats.
|
|
10
16
|
"""
|
|
11
17
|
|
|
12
18
|
from __future__ import annotations
|
|
13
19
|
|
|
14
20
|
import re
|
|
15
21
|
|
|
16
|
-
from cc_transcript.mining.formats import ReviewComment
|
|
22
|
+
from cc_transcript.mining.formats import ReviewComment
|
|
23
|
+
from cc_transcript.mining.spec import CallableReviewFormat, RegexReviewFormat, ReviewSpec
|
|
17
24
|
|
|
18
25
|
SUPERSET_INLINE_RE = re.compile(r"^In ((?=\S*[./]|\S+?:L)\S+?)(?::L(\d+)(?:-(\d+))?)?: (.+)$", re.MULTILINE)
|
|
19
|
-
CONDUCTOR_FINDING_RE = re.compile(
|
|
20
|
-
r"^- file: (?P<file>\S+?):(?P<line>\d+)\s*$"
|
|
21
|
-
r"(?:\n- theme: .+$)?"
|
|
22
|
-
r"(?:\n- claim: (?P<claim>.+)$)?"
|
|
23
|
-
r"(?:\n- suggestion: (?P<suggestion>.+)$)?",
|
|
24
|
-
re.MULTILINE,
|
|
25
|
-
)
|
|
26
26
|
CONDUCTOR_WORKSTREAM_HEADER_RE = re.compile(
|
|
27
27
|
r"^### (?P<id>[A-Z][\w-]*\d*)\s*\[(?P<kind>[A-Z]+)\]\s*—\s*(?P<title>.+)$",
|
|
28
28
|
re.MULTILINE,
|
|
29
29
|
)
|
|
30
30
|
WORKSTREAM_BODY_RE = re.compile(r"^(?:FIX|Tests): .+$", re.MULTILINE)
|
|
31
|
+
CONDUCTOR_FINDING_FMT = RegexReviewFormat(
|
|
32
|
+
name="conductor-finding",
|
|
33
|
+
groups=(
|
|
34
|
+
(
|
|
35
|
+
"conductor-finding",
|
|
36
|
+
r"^- file: (\S+?):(\d+)\s*$"
|
|
37
|
+
r"(?:\n- theme: .+$)?"
|
|
38
|
+
r"(?:\n- claim: (.+)$)?"
|
|
39
|
+
r"(?:\n- suggestion: (.+)$)?",
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
file_group=1,
|
|
43
|
+
line_start_group=2,
|
|
44
|
+
line_end_group=None,
|
|
45
|
+
comment_groups=(3, 4),
|
|
46
|
+
join=" ",
|
|
47
|
+
multiline=True,
|
|
48
|
+
ignore_case=False,
|
|
49
|
+
)
|
|
31
50
|
|
|
32
51
|
|
|
33
52
|
def extract_superset_inline(text: str) -> tuple[ReviewComment, ...]:
|
|
@@ -42,18 +61,6 @@ def extract_superset_inline(text: str) -> tuple[ReviewComment, ...]:
|
|
|
42
61
|
)
|
|
43
62
|
|
|
44
63
|
|
|
45
|
-
def extract_conductor_finding(text: str) -> tuple[ReviewComment, ...]:
|
|
46
|
-
return tuple(
|
|
47
|
-
ReviewComment(
|
|
48
|
-
file=match.group("file"),
|
|
49
|
-
line_start=int(match.group("line")),
|
|
50
|
-
line_end=None,
|
|
51
|
-
comment=" ".join(part.strip() for part in (match.group("claim"), match.group("suggestion")) if part),
|
|
52
|
-
)
|
|
53
|
-
for match in CONDUCTOR_FINDING_RE.finditer(text)
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
64
|
def extract_conductor_workstream(text: str) -> tuple[ReviewComment, ...]:
|
|
58
65
|
headers = list(CONDUCTOR_WORKSTREAM_HEADER_RE.finditer(text))
|
|
59
66
|
return tuple(
|
|
@@ -74,9 +81,13 @@ def extract_conductor_workstream(text: str) -> tuple[ReviewComment, ...]:
|
|
|
74
81
|
)
|
|
75
82
|
|
|
76
83
|
|
|
77
|
-
def
|
|
78
|
-
return (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
def review_spec() -> ReviewSpec:
|
|
85
|
+
return ReviewSpec(
|
|
86
|
+
regex_formats=(CONDUCTOR_FINDING_FMT,),
|
|
87
|
+
callable_formats=(
|
|
88
|
+
CallableReviewFormat("superset-inline", SUPERSET_INLINE_RE, extract_superset_inline),
|
|
89
|
+
CallableReviewFormat("conductor-workstream", CONDUCTOR_WORKSTREAM_HEADER_RE, extract_conductor_workstream),
|
|
90
|
+
),
|
|
91
|
+
structured_formats=(),
|
|
92
|
+
surfaces=frozenset({"typed"}),
|
|
82
93
|
)
|
|
@@ -58,20 +58,14 @@ from cc_transcript.filterspec import (
|
|
|
58
58
|
from cc_transcript.ids import EventRef
|
|
59
59
|
from cc_transcript.mining.candidates import FeedbackCandidate, dedup_key
|
|
60
60
|
from cc_transcript.mining.filterspec import at_least, build_candidate_filter, keep_candidate
|
|
61
|
-
from cc_transcript.mining.signals import
|
|
62
|
-
|
|
63
|
-
iter_plan_reentry_signals,
|
|
64
|
-
iter_plan_rejection_signals,
|
|
65
|
-
iter_review_comment_signals,
|
|
66
|
-
iter_tool_denial_signals,
|
|
67
|
-
iter_user_message_signals,
|
|
68
|
-
)
|
|
61
|
+
from cc_transcript.mining.signals import mine
|
|
62
|
+
from cc_transcript.mining.spec import MiningSpec
|
|
69
63
|
from cc_transcript.models import UserEvent
|
|
70
64
|
from cc_transcript.parser import TranscriptParser
|
|
71
65
|
|
|
72
66
|
from captain_hook.decisions import decisions_db_path, open_decision_log
|
|
73
67
|
from captain_hook.review.fix import HOOK_COMPLAINT, iter_hook_complaint_signals
|
|
74
|
-
from captain_hook.review.formats import
|
|
68
|
+
from captain_hook.review.formats import review_spec
|
|
75
69
|
from captain_hook.review.repo import resolve_repo_key
|
|
76
70
|
from captain_hook.review.store import CandidateKind
|
|
77
71
|
|
|
@@ -92,6 +86,14 @@ REVIEWER_MARKER = "capt-hook-session-reviewer"
|
|
|
92
86
|
|
|
93
87
|
SPEC_DETECTORS = frozenset({"transcript_message", "plan_reentry", "review_comment"})
|
|
94
88
|
|
|
89
|
+
REVIEWER_MINING_SPEC = MiningSpec(review=review_spec())
|
|
90
|
+
"""The reviewer's mining policy: all six core detectors with the reviewer's review formats.
|
|
91
|
+
|
|
92
|
+
Scoring, provenance, reentry lookback, and edit tools take the :class:`MiningSpec`
|
|
93
|
+
defaults; only the review-comment policy (the three reviewer formats, ``typed``
|
|
94
|
+
surfaces, no structured formats) is customized.
|
|
95
|
+
"""
|
|
96
|
+
|
|
95
97
|
STRICT_USER: FilterSpec = build_spec(
|
|
96
98
|
keep_only("user"),
|
|
97
99
|
drop_sidechain(),
|
|
@@ -191,23 +193,16 @@ def to_candidate(activity: SessionActivity, sig: MiningSignal) -> FeedbackCandid
|
|
|
191
193
|
|
|
192
194
|
|
|
193
195
|
def detect(events: Sequence[TranscriptEvent]) -> Iterator[MiningSignal]:
|
|
194
|
-
"""
|
|
196
|
+
"""Mines all six neutral detectors over one transcript's events.
|
|
195
197
|
|
|
196
198
|
Args:
|
|
197
199
|
events: The transcript's full ordered event stream.
|
|
198
200
|
|
|
199
201
|
Returns:
|
|
200
202
|
Every mining signal the detectors recognize, ungated; review comments
|
|
201
|
-
run under the reviewer's :
|
|
203
|
+
run under the reviewer's :data:`REVIEWER_MINING_SPEC`.
|
|
202
204
|
"""
|
|
203
|
-
return
|
|
204
|
-
iter_user_message_signals(events),
|
|
205
|
-
iter_plan_rejection_signals(events),
|
|
206
|
-
iter_plan_reentry_signals(events),
|
|
207
|
-
iter_tool_denial_signals(events),
|
|
208
|
-
iter_interrupt_marker_signals(events),
|
|
209
|
-
iter_review_comment_signals(events, formats(), surfaces=frozenset({"typed"}), structured_formats=()),
|
|
210
|
-
)
|
|
205
|
+
return mine(events, REVIEWER_MINING_SPEC)
|
|
211
206
|
|
|
212
207
|
|
|
213
208
|
def candidates_from(
|
|
@@ -122,9 +122,13 @@ def make_pre_tool_event(
|
|
|
122
122
|
tool_name: str = "Bash",
|
|
123
123
|
tool_input: dict[str, Any] | None = None,
|
|
124
124
|
ctx: Any = None,
|
|
125
|
+
*,
|
|
126
|
+
permission_mode: str | None = None,
|
|
125
127
|
) -> PreToolUseEvent:
|
|
126
128
|
return PreToolUseEvent(
|
|
127
|
-
_raw={"tool_name": tool_name}
|
|
129
|
+
_raw={"tool_name": tool_name}
|
|
130
|
+
| ({"tool_input": tool_input} if tool_input is not None else {})
|
|
131
|
+
| ({"permission_mode": permission_mode} if permission_mode is not None else {}),
|
|
128
132
|
ctx=ctx or make_ctx(),
|
|
129
133
|
)
|
|
130
134
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from cc_transcript import parse_events_from_bytes
|
|
11
|
+
|
|
12
|
+
from captain_hook.review.judge import ReviewVerdict
|
|
13
|
+
from captain_hook.review.repo import RepoKey
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from cc_transcript.models import TranscriptEvent
|
|
19
|
+
|
|
20
|
+
from captain_hook.review.judge import Category
|
|
21
|
+
|
|
22
|
+
REPO = RepoKey("github.com/yasyf/captain-hook")
|
|
23
|
+
BASE_TS = "2026-06-01T12:00:00+00:00"
|
|
24
|
+
CORRECTION = "no, never use a bare except here, always catch the specific parser error"
|
|
25
|
+
PROMPT_VERSION = 1
|
|
26
|
+
|
|
27
|
+
REVIEW_UUIDS = itertools.count()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class Verdict:
|
|
32
|
+
accepted: bool = True
|
|
33
|
+
confidence: float = 0.9
|
|
34
|
+
category: str = "durable_correction"
|
|
35
|
+
summary: str = "user corrected approach"
|
|
36
|
+
rationale: str = "explicit correction"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def next_uuid() -> str:
|
|
40
|
+
return f"uuid-{next(REVIEW_UUIDS)}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def envelope(entry_type: str, **overrides: Any) -> dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"type": entry_type,
|
|
46
|
+
"uuid": overrides.pop("uuid", next_uuid()),
|
|
47
|
+
"parentUuid": None,
|
|
48
|
+
"sessionId": overrides.pop("sessionId", "sess-1"),
|
|
49
|
+
"timestamp": overrides.pop("timestamp", BASE_TS),
|
|
50
|
+
"cwd": overrides.pop("cwd", "/repo"),
|
|
51
|
+
"gitBranch": "main",
|
|
52
|
+
"version": "1.2.3",
|
|
53
|
+
"isSidechain": False,
|
|
54
|
+
"isMeta": False,
|
|
55
|
+
"entrypoint": "cli",
|
|
56
|
+
**overrides,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def user_text(text: str, **overrides: Any) -> dict[str, Any]:
|
|
61
|
+
return envelope("user", message={"role": "user", "content": text}, **overrides)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def assistant_text(text: str, **overrides: Any) -> dict[str, Any]:
|
|
65
|
+
return envelope(
|
|
66
|
+
"assistant",
|
|
67
|
+
message={"role": "assistant", "model": "claude", "content": [{"type": "text", "text": text}]},
|
|
68
|
+
**overrides,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def assistant_tool_use(tool_id: str, name: str, tool_input: dict[str, Any], **overrides: Any) -> dict[str, Any]:
|
|
73
|
+
return envelope(
|
|
74
|
+
"assistant",
|
|
75
|
+
message={
|
|
76
|
+
"role": "assistant",
|
|
77
|
+
"model": "claude",
|
|
78
|
+
"content": [{"type": "tool_use", "id": tool_id, "name": name, "input": tool_input}],
|
|
79
|
+
},
|
|
80
|
+
**overrides,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def tool_result(tool_id: str, content: str, *, is_error: bool = False, **overrides: Any) -> dict[str, Any]:
|
|
85
|
+
return envelope(
|
|
86
|
+
"user",
|
|
87
|
+
message={
|
|
88
|
+
"role": "user",
|
|
89
|
+
"content": [{"type": "tool_result", "tool_use_id": tool_id, "content": content, "is_error": is_error}],
|
|
90
|
+
},
|
|
91
|
+
**overrides,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def correction_entries(*, session: str = "sess-1", timestamp: str = BASE_TS, **overrides: Any) -> list[dict[str, Any]]:
|
|
96
|
+
return [
|
|
97
|
+
assistant_text("I'll wrap the parser in a bare except", sessionId=session, timestamp=timestamp, **overrides),
|
|
98
|
+
user_text(CORRECTION, sessionId=session, timestamp=timestamp, **overrides),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse(entries: list[dict[str, Any]]) -> list[TranscriptEvent]:
|
|
103
|
+
return parse_events_from_bytes("".join(json.dumps(entry) + "\n" for entry in entries).encode())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def write_transcript(path: Path, entries: list[dict[str, Any]]) -> Path:
|
|
107
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
path.write_text("".join(json.dumps(entry) + "\n" for entry in entries))
|
|
109
|
+
return path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def llm_backend_available() -> bool:
|
|
113
|
+
from cc_transcript.judge.llm import default_backend
|
|
114
|
+
from spawnllm import BackendUnavailable
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
default_backend()
|
|
118
|
+
except BackendUnavailable:
|
|
119
|
+
return False
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
requires_llm_backend = pytest.mark.skipif(
|
|
124
|
+
not llm_backend_available(),
|
|
125
|
+
reason="no installed, authenticated LLM backend (spawnllm.select_backend raised BackendUnavailable)",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def install_judge(
|
|
130
|
+
monkeypatch: pytest.MonkeyPatch, *, category: Category = "durable_style_rule", fail_on: str | None = None
|
|
131
|
+
) -> list[str]:
|
|
132
|
+
calls: list[str] = []
|
|
133
|
+
|
|
134
|
+
async def judge(prompt: str) -> ReviewVerdict:
|
|
135
|
+
calls.append(prompt)
|
|
136
|
+
if fail_on is not None and fail_on in prompt:
|
|
137
|
+
raise subprocess.CalledProcessError(1, ["claude"])
|
|
138
|
+
return ReviewVerdict(category=category, summary="states a durable rule", confidence=0.9, rationale="r")
|
|
139
|
+
|
|
140
|
+
monkeypatch.setattr("captain_hook.review.judge.structured_judge", lambda *_, **__: judge)
|
|
141
|
+
return calls
|
|
@@ -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
|
+
version = "4.0.0"
|
|
5
5
|
description = "Declarative hook framework for Claude Code"
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
requires-python = ">=3.13"
|
|
23
23
|
dependencies = [
|
|
24
|
-
"cc-transcript>=
|
|
24
|
+
"cc-transcript>=7,<8",
|
|
25
25
|
"pydantic>=2.0",
|
|
26
26
|
"pydantic-settings>=2.0",
|
|
27
27
|
"tree-sitter>=0.24",
|
|
@@ -36,7 +36,7 @@ dependencies = [
|
|
|
36
36
|
"lazy-object-proxy>=1.12.0",
|
|
37
37
|
"filelock>=3",
|
|
38
38
|
"loguru>=0.7.3",
|
|
39
|
-
"spawnllm>=0.
|
|
39
|
+
"spawnllm>=0.5.0",
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
[project.optional-dependencies]
|
|
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-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md
RENAMED
|
File without changes
|
{capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-3.20.0 → capt_hook-4.0.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md
RENAMED
|
File without changes
|
{capt_hook-3.20.0 → capt_hook-4.0.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
|