capt-hook 0.3.0__tar.gz → 0.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.
- {capt_hook-0.3.0 → capt_hook-0.4.0}/PKG-INFO +15 -5
- {capt_hook-0.3.0 → capt_hook-0.4.0}/README.md +12 -3
- capt_hook-0.4.0/captain_hook/.claude-plugin/plugin.json +10 -0
- capt_hook-0.4.0/captain_hook/__init__.py +257 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/cli.py +93 -2
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/context.py +3 -3
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/primitives/__init__.py +0 -20
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/primitives/llm.py +3 -3
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/prompt.py +10 -11
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/signals/__init__.py +0 -11
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/signals/nlp.py +0 -3
- capt_hook-0.4.0/captain_hook/skills/bootstrapping-hooks/SKILL.md +229 -0
- capt_hook-0.4.0/captain_hook/skills/bootstrapping-hooks/references/capt-hook-api.md +166 -0
- capt_hook-0.4.0/captain_hook/skills/bootstrapping-hooks/references/pattern-catalog.md +404 -0
- capt_hook-0.4.0/captain_hook/skills/bootstrapping-hooks/references/testing-hooks.md +92 -0
- capt_hook-0.4.0/captain_hook/skills/translating-styleguides/SKILL.md +366 -0
- capt_hook-0.4.0/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +136 -0
- capt_hook-0.4.0/captain_hook/skills/translating-styleguides/references/matcher-reference.md +126 -0
- capt_hook-0.4.0/captain_hook/skills/translating-styleguides/references/tier-rubric.md +91 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/style/__init__.py +4 -13
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/style/matchers.py +0 -27
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/templates/example_hook.py.tmpl +9 -11
- capt_hook-0.4.0/captain_hook/testing/__init__.py +18 -0
- capt_hook-0.4.0/captain_hook/tests/__init__.py +33 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/tests/helpers.py +28 -50
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/transcript/__init__.py +7 -25
- {capt_hook-0.3.0 → capt_hook-0.4.0}/pyproject.toml +6 -8
- capt_hook-0.3.0/captain_hook/__init__.py +0 -240
- capt_hook-0.3.0/captain_hook/testing/__init__.py +0 -10
- capt_hook-0.3.0/captain_hook/tests/__init__.py +0 -27
- {capt_hook-0.3.0 → capt_hook-0.4.0}/LICENSE +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/__main__.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/app.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/command.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/conditions.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/events.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/file.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/llm/__init__.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/llm/backends.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/loader.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/log.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/primitives/audit.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/py.typed +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/session.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/settings.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/state.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/style/types.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/tasks.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/tools.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/transcript/inputs.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/transcript/models.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/types.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-0.3.0 → capt_hook-0.4.0}/captain_hook/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capt-hook
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -32,10 +32,11 @@ Requires-Dist: loguru>=0.7.3
|
|
|
32
32
|
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
33
33
|
Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
|
|
34
34
|
Requires-Dist: pyright>=1.1 ; extra == 'dev'
|
|
35
|
+
Requires-Dist: pyyaml>=6 ; extra == 'dev'
|
|
35
36
|
Requires-Dist: ruff>=0.8 ; extra == 'dev'
|
|
36
37
|
Requires-Python: >=3.12
|
|
37
38
|
Project-URL: Homepage, https://github.com/yasyf/captain-hook
|
|
38
|
-
Project-URL: Documentation, https://captain-hook
|
|
39
|
+
Project-URL: Documentation, https://yasyf.github.io/captain-hook/
|
|
39
40
|
Project-URL: Repository, https://github.com/yasyf/captain-hook
|
|
40
41
|
Project-URL: Issues, https://github.com/yasyf/captain-hook/issues
|
|
41
42
|
Project-URL: Changelog, https://github.com/yasyf/captain-hook/blob/main/CHANGELOG.md
|
|
@@ -46,7 +47,7 @@ Description-Content-Type: text/markdown
|
|
|
46
47
|
|
|
47
48
|
[](https://pypi.org/project/capt-hook/)
|
|
48
49
|
[](https://pypi.org/project/capt-hook/)
|
|
49
|
-
[](https://yasyf.github.io/captain-hook/)
|
|
50
51
|
[](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
|
|
51
52
|
|
|
52
53
|
Declarative hook framework for Claude Code. Write hooks as data, test them inline, and ship them to CI in the same shape they run in production.
|
|
@@ -99,6 +100,15 @@ capt-hook generate-settings > .claude/settings.local.json
|
|
|
99
100
|
|
|
100
101
|
The next time Claude tries `git stash`, captain-hook returns a deny with your reason and hint.
|
|
101
102
|
|
|
103
|
+
## Agent skills & plugin
|
|
104
|
+
|
|
105
|
+
Don't want to write hooks by hand? capt-hook ships two [Agent Skills](https://yasyf.github.io/captain-hook/docs/getting-started/skills.html) — `bootstrapping-hooks` mines your repo's docs, CI, and git history into proposed gates and nudges; `translating-styleguides` turns a STYLEGUIDE.md into enforced rules. `uvx capt-hook init` installs them into `.claude/skills/`, or get them as a plugin:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
/plugin marketplace add yasyf/captain-hook
|
|
109
|
+
/plugin install captain-hook@captain-hook
|
|
110
|
+
```
|
|
111
|
+
|
|
102
112
|
## What problems does this solve?
|
|
103
113
|
|
|
104
114
|
- **Block dangerous tool calls** before they execute (`PreToolUse`) — force-push, package-manager footguns, raw `rm -rf`.
|
|
@@ -108,6 +118,6 @@ The next time Claude tries `git stash`, captain-hook returns a deny with your re
|
|
|
108
118
|
|
|
109
119
|
## Docs
|
|
110
120
|
|
|
111
|
-
[Read the docs](https://captain-hook
|
|
121
|
+
[Read the docs](https://yasyf.github.io/captain-hook/) for the full guide: conditions, primitives, LLM hooks, workflows, state, and real-world patterns.
|
|
112
122
|
|
|
113
|
-
Working on captain-hook itself? See the [development guide](https://
|
|
123
|
+
Working on captain-hook itself? See the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/capt-hook/)
|
|
4
4
|
[](https://pypi.org/project/capt-hook/)
|
|
5
|
-
[](https://yasyf.github.io/captain-hook/)
|
|
6
6
|
[](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
|
|
7
7
|
|
|
8
8
|
Declarative hook framework for Claude Code. Write hooks as data, test them inline, and ship them to CI in the same shape they run in production.
|
|
@@ -55,6 +55,15 @@ capt-hook generate-settings > .claude/settings.local.json
|
|
|
55
55
|
|
|
56
56
|
The next time Claude tries `git stash`, captain-hook returns a deny with your reason and hint.
|
|
57
57
|
|
|
58
|
+
## Agent skills & plugin
|
|
59
|
+
|
|
60
|
+
Don't want to write hooks by hand? capt-hook ships two [Agent Skills](https://yasyf.github.io/captain-hook/docs/getting-started/skills.html) — `bootstrapping-hooks` mines your repo's docs, CI, and git history into proposed gates and nudges; `translating-styleguides` turns a STYLEGUIDE.md into enforced rules. `uvx capt-hook init` installs them into `.claude/skills/`, or get them as a plugin:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
/plugin marketplace add yasyf/captain-hook
|
|
64
|
+
/plugin install captain-hook@captain-hook
|
|
65
|
+
```
|
|
66
|
+
|
|
58
67
|
## What problems does this solve?
|
|
59
68
|
|
|
60
69
|
- **Block dangerous tool calls** before they execute (`PreToolUse`) — force-push, package-manager footguns, raw `rm -rf`.
|
|
@@ -64,6 +73,6 @@ The next time Claude tries `git stash`, captain-hook returns a deny with your re
|
|
|
64
73
|
|
|
65
74
|
## Docs
|
|
66
75
|
|
|
67
|
-
[Read the docs](https://captain-hook
|
|
76
|
+
[Read the docs](https://yasyf.github.io/captain-hook/) for the full guide: conditions, primitives, LLM hooks, workflows, state, and real-world patterns.
|
|
68
77
|
|
|
69
|
-
Working on captain-hook itself? See the [development guide](https://
|
|
78
|
+
Working on captain-hook itself? See the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "captain-hook",
|
|
3
|
+
"displayName": "Captain Hook",
|
|
4
|
+
"description": "Skills for the capt-hook declarative hook framework: bootstrap gates and nudges from a repo's conventions, and translate style guides into enforced StyleRules.",
|
|
5
|
+
"author": { "name": "Yasyf Mohamedali", "email": "yasyfm@gmail.com" },
|
|
6
|
+
"homepage": "https://yasyf.github.io/captain-hook/",
|
|
7
|
+
"repository": "https://github.com/yasyf/captain-hook",
|
|
8
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
9
|
+
"keywords": ["claude-code", "hooks", "guardrails", "styleguide"]
|
|
10
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from captain_hook import style as style
|
|
4
|
+
from captain_hook.app import hook as hook
|
|
5
|
+
from captain_hook.app import on as on
|
|
6
|
+
from captain_hook.app import register as register
|
|
7
|
+
from captain_hook.command import Command as Command
|
|
8
|
+
from captain_hook.command import CommandLine as CommandLine
|
|
9
|
+
from captain_hook.command import Redirect as Redirect
|
|
10
|
+
from captain_hook.context import HookContext as HookContext
|
|
11
|
+
from captain_hook.events import (
|
|
12
|
+
BaseHookEvent as BaseHookEvent,
|
|
13
|
+
)
|
|
14
|
+
from captain_hook.events import (
|
|
15
|
+
NotificationEvent as NotificationEvent,
|
|
16
|
+
)
|
|
17
|
+
from captain_hook.events import (
|
|
18
|
+
PostToolUseEvent as PostToolUseEvent,
|
|
19
|
+
)
|
|
20
|
+
from captain_hook.events import (
|
|
21
|
+
PostToolUseFailureEvent as PostToolUseFailureEvent,
|
|
22
|
+
)
|
|
23
|
+
from captain_hook.events import (
|
|
24
|
+
PreCompactEvent as PreCompactEvent,
|
|
25
|
+
)
|
|
26
|
+
from captain_hook.events import (
|
|
27
|
+
PreToolUseEvent as PreToolUseEvent,
|
|
28
|
+
)
|
|
29
|
+
from captain_hook.events import (
|
|
30
|
+
StopEvent as StopEvent,
|
|
31
|
+
)
|
|
32
|
+
from captain_hook.events import (
|
|
33
|
+
SubagentStartEvent as SubagentStartEvent,
|
|
34
|
+
)
|
|
35
|
+
from captain_hook.events import (
|
|
36
|
+
SubagentStopEvent as SubagentStopEvent,
|
|
37
|
+
)
|
|
38
|
+
from captain_hook.events import (
|
|
39
|
+
ToolHookEvent as ToolHookEvent,
|
|
40
|
+
)
|
|
41
|
+
from captain_hook.events import (
|
|
42
|
+
UserPromptSubmitEvent as UserPromptSubmitEvent,
|
|
43
|
+
)
|
|
44
|
+
from captain_hook.file import File as File
|
|
45
|
+
from captain_hook.primitives import (
|
|
46
|
+
GateVerdict as GateVerdict,
|
|
47
|
+
)
|
|
48
|
+
from captain_hook.primitives import (
|
|
49
|
+
NudgeVerdict as NudgeVerdict,
|
|
50
|
+
)
|
|
51
|
+
from captain_hook.primitives import (
|
|
52
|
+
PromptCheckVerdict as PromptCheckVerdict,
|
|
53
|
+
)
|
|
54
|
+
from captain_hook.primitives import (
|
|
55
|
+
block_command as block_command,
|
|
56
|
+
)
|
|
57
|
+
from captain_hook.primitives import (
|
|
58
|
+
gate as gate,
|
|
59
|
+
)
|
|
60
|
+
from captain_hook.primitives import (
|
|
61
|
+
llm_evaluate as llm_evaluate,
|
|
62
|
+
)
|
|
63
|
+
from captain_hook.primitives import (
|
|
64
|
+
llm_gate as llm_gate,
|
|
65
|
+
)
|
|
66
|
+
from captain_hook.primitives import (
|
|
67
|
+
llm_nudge as llm_nudge,
|
|
68
|
+
)
|
|
69
|
+
from captain_hook.primitives import (
|
|
70
|
+
prompt_check as prompt_check,
|
|
71
|
+
)
|
|
72
|
+
from captain_hook.primitives import (
|
|
73
|
+
warn_command as warn_command,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# audit/lint/nudge are imported from their defining modules, not the
|
|
77
|
+
# primitives package: the package attribute and the submodule share a name,
|
|
78
|
+
# and an alias targeting captain_hook.primitives.<name> resolves to the
|
|
79
|
+
# module under static analysis (griffe), shadowing the function.
|
|
80
|
+
from captain_hook.primitives.audit import audit as audit
|
|
81
|
+
from captain_hook.primitives.lint import lint as lint
|
|
82
|
+
from captain_hook.primitives.nudge import nudge as nudge
|
|
83
|
+
from captain_hook.primitives.workflow import (
|
|
84
|
+
Artifact as Artifact,
|
|
85
|
+
)
|
|
86
|
+
from captain_hook.primitives.workflow import (
|
|
87
|
+
Step as Step,
|
|
88
|
+
)
|
|
89
|
+
from captain_hook.primitives.workflow import (
|
|
90
|
+
Workflow as Workflow,
|
|
91
|
+
)
|
|
92
|
+
from captain_hook.primitives.workflow import (
|
|
93
|
+
text_matches as text_matches,
|
|
94
|
+
)
|
|
95
|
+
from captain_hook.primitives.workflow import (
|
|
96
|
+
workflow as workflow,
|
|
97
|
+
)
|
|
98
|
+
from captain_hook.prompt import Prompt as Prompt
|
|
99
|
+
from captain_hook.session import SessionSlot as SessionSlot
|
|
100
|
+
from captain_hook.session import SessionStore as SessionStore
|
|
101
|
+
from captain_hook.session import session_state as session_state
|
|
102
|
+
from captain_hook.settings import HooksSettings as HooksSettings
|
|
103
|
+
from captain_hook.settings import build_settings as build_settings
|
|
104
|
+
from captain_hook.signals.nlp import Clause as Clause
|
|
105
|
+
from captain_hook.signals.nlp import NlpSignal as NlpSignal
|
|
106
|
+
from captain_hook.signals.nlp import Phrase as Phrase
|
|
107
|
+
from captain_hook.state import (
|
|
108
|
+
HookState as HookState,
|
|
109
|
+
)
|
|
110
|
+
from captain_hook.state import (
|
|
111
|
+
PrimitiveState as PrimitiveState,
|
|
112
|
+
)
|
|
113
|
+
from captain_hook.state import (
|
|
114
|
+
workflow_state as workflow_state,
|
|
115
|
+
)
|
|
116
|
+
from captain_hook.tasks import Task as Task
|
|
117
|
+
from captain_hook.tasks import Tasks as Tasks
|
|
118
|
+
from captain_hook.testing import (
|
|
119
|
+
Allow as Allow,
|
|
120
|
+
)
|
|
121
|
+
from captain_hook.testing import (
|
|
122
|
+
Block as Block,
|
|
123
|
+
)
|
|
124
|
+
from captain_hook.testing import (
|
|
125
|
+
InlineTests as InlineTests,
|
|
126
|
+
)
|
|
127
|
+
from captain_hook.testing import (
|
|
128
|
+
Input as Input,
|
|
129
|
+
)
|
|
130
|
+
from captain_hook.testing import (
|
|
131
|
+
TranscriptFixture as TranscriptFixture,
|
|
132
|
+
)
|
|
133
|
+
from captain_hook.testing import (
|
|
134
|
+
Warn as Warn,
|
|
135
|
+
)
|
|
136
|
+
from captain_hook.tools import EditOp as EditOp
|
|
137
|
+
from captain_hook.tools import TaskOp as TaskOp
|
|
138
|
+
from captain_hook.tools import WriteOp as WriteOp
|
|
139
|
+
from captain_hook.transcript import (
|
|
140
|
+
ToolUse as ToolUse,
|
|
141
|
+
)
|
|
142
|
+
from captain_hook.transcript import (
|
|
143
|
+
ToolUseQuery as ToolUseQuery,
|
|
144
|
+
)
|
|
145
|
+
from captain_hook.transcript import (
|
|
146
|
+
ToolUseSequence as ToolUseSequence,
|
|
147
|
+
)
|
|
148
|
+
from captain_hook.transcript import (
|
|
149
|
+
Transcript as Transcript,
|
|
150
|
+
)
|
|
151
|
+
from captain_hook.transcript import (
|
|
152
|
+
TranscriptMessage as TranscriptMessage,
|
|
153
|
+
)
|
|
154
|
+
from captain_hook.transcript import (
|
|
155
|
+
TranscriptSlice as TranscriptSlice,
|
|
156
|
+
)
|
|
157
|
+
from captain_hook.transcript import (
|
|
158
|
+
Turn as Turn,
|
|
159
|
+
)
|
|
160
|
+
from captain_hook.transcript.inputs import (
|
|
161
|
+
AgentInput as AgentInput,
|
|
162
|
+
)
|
|
163
|
+
from captain_hook.transcript.inputs import (
|
|
164
|
+
BashInput as BashInput,
|
|
165
|
+
)
|
|
166
|
+
from captain_hook.transcript.inputs import (
|
|
167
|
+
EditInput as EditInput,
|
|
168
|
+
)
|
|
169
|
+
from captain_hook.transcript.inputs import (
|
|
170
|
+
FileInputBase as FileInputBase,
|
|
171
|
+
)
|
|
172
|
+
from captain_hook.transcript.inputs import (
|
|
173
|
+
GenericInput as GenericInput,
|
|
174
|
+
)
|
|
175
|
+
from captain_hook.transcript.inputs import (
|
|
176
|
+
GlobInput as GlobInput,
|
|
177
|
+
)
|
|
178
|
+
from captain_hook.transcript.inputs import (
|
|
179
|
+
GrepInput as GrepInput,
|
|
180
|
+
)
|
|
181
|
+
from captain_hook.transcript.inputs import (
|
|
182
|
+
InputBase as InputBase,
|
|
183
|
+
)
|
|
184
|
+
from captain_hook.transcript.inputs import (
|
|
185
|
+
ReadInput as ReadInput,
|
|
186
|
+
)
|
|
187
|
+
from captain_hook.transcript.inputs import (
|
|
188
|
+
SkillInput as SkillInput,
|
|
189
|
+
)
|
|
190
|
+
from captain_hook.transcript.inputs import (
|
|
191
|
+
TaskCreateInput as TaskCreateInput,
|
|
192
|
+
)
|
|
193
|
+
from captain_hook.transcript.inputs import (
|
|
194
|
+
TaskUpdateInput as TaskUpdateInput,
|
|
195
|
+
)
|
|
196
|
+
from captain_hook.transcript.inputs import (
|
|
197
|
+
WriteInput as WriteInput,
|
|
198
|
+
)
|
|
199
|
+
from captain_hook.transcript.models import ContentBlock as ContentBlock
|
|
200
|
+
from captain_hook.transcript.models import ToolResult as ToolResult
|
|
201
|
+
from captain_hook.types import (
|
|
202
|
+
Action as Action,
|
|
203
|
+
)
|
|
204
|
+
from captain_hook.types import (
|
|
205
|
+
Agent as Agent,
|
|
206
|
+
)
|
|
207
|
+
from captain_hook.types import (
|
|
208
|
+
Content as Content,
|
|
209
|
+
)
|
|
210
|
+
from captain_hook.types import (
|
|
211
|
+
CustomCondition as CustomCondition,
|
|
212
|
+
)
|
|
213
|
+
from captain_hook.types import (
|
|
214
|
+
Event as Event,
|
|
215
|
+
)
|
|
216
|
+
from captain_hook.types import (
|
|
217
|
+
FilePath as FilePath,
|
|
218
|
+
)
|
|
219
|
+
from captain_hook.types import (
|
|
220
|
+
HookResult as HookResult,
|
|
221
|
+
)
|
|
222
|
+
from captain_hook.types import (
|
|
223
|
+
InPlanMode as InPlanMode,
|
|
224
|
+
)
|
|
225
|
+
from captain_hook.types import (
|
|
226
|
+
RanCommand as RanCommand,
|
|
227
|
+
)
|
|
228
|
+
from captain_hook.types import (
|
|
229
|
+
ReadFile as ReadFile,
|
|
230
|
+
)
|
|
231
|
+
from captain_hook.types import (
|
|
232
|
+
Signal as Signal,
|
|
233
|
+
)
|
|
234
|
+
from captain_hook.types import (
|
|
235
|
+
Signals as Signals,
|
|
236
|
+
)
|
|
237
|
+
from captain_hook.types import (
|
|
238
|
+
SourceEdits as SourceEdits,
|
|
239
|
+
)
|
|
240
|
+
from captain_hook.types import (
|
|
241
|
+
TCondition as TCondition,
|
|
242
|
+
)
|
|
243
|
+
from captain_hook.types import (
|
|
244
|
+
TestFile as TestFile,
|
|
245
|
+
)
|
|
246
|
+
from captain_hook.types import (
|
|
247
|
+
Tool as Tool,
|
|
248
|
+
)
|
|
249
|
+
from captain_hook.types import (
|
|
250
|
+
TouchedFile as TouchedFile,
|
|
251
|
+
)
|
|
252
|
+
from captain_hook.types import (
|
|
253
|
+
UsedSkill as UsedSkill,
|
|
254
|
+
)
|
|
255
|
+
from captain_hook.types import (
|
|
256
|
+
Waiting as Waiting,
|
|
257
|
+
)
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import importlib.resources
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
6
8
|
import sys
|
|
7
9
|
from collections import defaultdict
|
|
8
10
|
from dataclasses import dataclass
|
|
@@ -22,6 +24,8 @@ from captain_hook.types import Event
|
|
|
22
24
|
|
|
23
25
|
DIST_NAME = "capt-hook"
|
|
24
26
|
EVENT_NAMES = ", ".join(n for e in Event if (n := e.name))
|
|
27
|
+
MARKETPLACE = {"captain-hook": {"source": {"source": "github", "repo": "yasyf/captain-hook"}}}
|
|
28
|
+
PLUGIN_ID = "captain-hook@captain-hook"
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
@dataclass(frozen=True, slots=True)
|
|
@@ -40,6 +44,68 @@ def example_hook_source() -> str:
|
|
|
40
44
|
return (importlib.resources.files("captain_hook") / "templates" / "example_hook.py.tmpl").read_text()
|
|
41
45
|
|
|
42
46
|
|
|
47
|
+
def install_skills(root: Path, *, force: bool = False) -> dict[str, str]:
|
|
48
|
+
"""Copy the bundled Claude Code skills into ``root/.claude/skills``.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
root: Project root receiving the skills.
|
|
52
|
+
force: Replace existing skill directories wholesale instead of skipping them.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Per-skill status of ``"installed"``, ``"replaced"``, or ``"skipped"``.
|
|
56
|
+
"""
|
|
57
|
+
dest_root = root / ".claude" / "skills"
|
|
58
|
+
summary: dict[str, str] = {}
|
|
59
|
+
with importlib.resources.as_file(importlib.resources.files("captain_hook") / "skills") as src_root:
|
|
60
|
+
for skill in sorted(p for p in src_root.iterdir() if p.is_dir()):
|
|
61
|
+
dest = dest_root / skill.name
|
|
62
|
+
if dest.exists() and not force:
|
|
63
|
+
summary[skill.name] = "skipped"
|
|
64
|
+
continue
|
|
65
|
+
if dest.exists():
|
|
66
|
+
shutil.rmtree(dest)
|
|
67
|
+
summary[skill.name] = "replaced"
|
|
68
|
+
else:
|
|
69
|
+
summary[skill.name] = "installed"
|
|
70
|
+
shutil.copytree(skill, dest)
|
|
71
|
+
return summary
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def register_marketplace(root: Path) -> None:
|
|
75
|
+
"""Enable the captain-hook plugin marketplace in ``root/.claude/settings.local.json``.
|
|
76
|
+
|
|
77
|
+
Merges ``extraKnownMarketplaces`` and ``enabledPlugins`` entries into the
|
|
78
|
+
existing settings so the bundled skills track the repository as a plugin.
|
|
79
|
+
"""
|
|
80
|
+
settings_path = root / ".claude" / "settings.local.json"
|
|
81
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
83
|
+
merged = existing | {
|
|
84
|
+
"extraKnownMarketplaces": existing.get("extraKnownMarketplaces", {}) | MARKETPLACE,
|
|
85
|
+
"enabledPlugins": existing.get("enabledPlugins", {}) | {PLUGIN_ID: True},
|
|
86
|
+
}
|
|
87
|
+
settings_path.write_text(json.dumps(merged, indent=2) + "\n")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def maybe_launch_bootstrap(root: Path) -> bool:
|
|
91
|
+
"""Offer to launch Claude with the ``bootstrapping-hooks`` skill after ``init``.
|
|
92
|
+
|
|
93
|
+
Only fires in an interactive session with the ``claude`` CLI on PATH; CI and
|
|
94
|
+
scripted runs skip the prompt entirely. On acceptance, the captain-hook plugin
|
|
95
|
+
marketplace is registered in ``.claude/settings.local.json`` before launching.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Whether Claude was launched.
|
|
99
|
+
"""
|
|
100
|
+
if not (sys.stdin.isatty() and shutil.which("claude")):
|
|
101
|
+
return False
|
|
102
|
+
if not click.confirm("Bootstrap hooks now? (launches Claude with the bootstrapping-hooks skill)", default=True):
|
|
103
|
+
return False
|
|
104
|
+
register_marketplace(root)
|
|
105
|
+
subprocess.run(["claude", "/bootstrapping-hooks"], cwd=root, check=False)
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
|
|
43
109
|
def generate_settings(hooks_dir: str = ".claude/hooks", from_source: str = DIST_NAME) -> dict[str, Any]:
|
|
44
110
|
events_by_async: defaultdict[bool, set[str]] = defaultdict(set)
|
|
45
111
|
for entry in _state.hooks:
|
|
@@ -172,6 +238,8 @@ def init_project(root: Path) -> None:
|
|
|
172
238
|
merged, summary = merge_init_settings(".claude/hooks", settings_path)
|
|
173
239
|
settings_path.write_text(json.dumps(merged, indent=2) + "\n")
|
|
174
240
|
|
|
241
|
+
skills_summary = install_skills(root)
|
|
242
|
+
|
|
175
243
|
print(f"Scaffolded {example.relative_to(root)} + {settings_path.relative_to(root)}.")
|
|
176
244
|
print()
|
|
177
245
|
print(f"{settings_path.relative_to(root)}:")
|
|
@@ -184,11 +252,20 @@ def init_project(root: Path) -> None:
|
|
|
184
252
|
if unchanged:
|
|
185
253
|
print(f" unchanged: {', '.join(unchanged)} (already present)")
|
|
186
254
|
print()
|
|
255
|
+
print(".claude/skills/:")
|
|
256
|
+
for name in (n for n, status in skills_summary.items() if status == "installed"):
|
|
257
|
+
print(f" + installed {name}")
|
|
258
|
+
if skipped := [n for n, status in skills_summary.items() if status == "skipped"]:
|
|
259
|
+
print(f" unchanged: {', '.join(skipped)} (already present; capt-hook skills install --force to refresh)")
|
|
260
|
+
print()
|
|
187
261
|
print("Next:")
|
|
188
|
-
print(" 1. Read the quickstart:
|
|
262
|
+
print(" 1. Read the quickstart: https://yasyf.github.io/captain-hook/")
|
|
189
263
|
print(" 2. Edit example.py or add new files under .claude/hooks/")
|
|
190
264
|
print(" 3. capt-hook test # verify inline tests")
|
|
191
265
|
print(" 4. capt-hook generate-settings # rewire after adding events")
|
|
266
|
+
print(" 5. /bootstrapping-hooks in Claude # mine hooks from this repo's conventions")
|
|
267
|
+
print()
|
|
268
|
+
maybe_launch_bootstrap(root)
|
|
192
269
|
|
|
193
270
|
|
|
194
271
|
def show_logs(session: str | None = None, tail: int | None = None) -> None:
|
|
@@ -345,7 +422,7 @@ def test(state: CliState, json_output: bool) -> None:
|
|
|
345
422
|
@cli.command()
|
|
346
423
|
@click.pass_obj
|
|
347
424
|
def init(state: CliState) -> None:
|
|
348
|
-
"""Scaffold hooks directory,
|
|
425
|
+
"""Scaffold the hooks directory, install bundled skills, and wire settings."""
|
|
349
426
|
init_project(state.root)
|
|
350
427
|
|
|
351
428
|
|
|
@@ -357,4 +434,18 @@ def logs(session: str | None, tail: int | None) -> None:
|
|
|
357
434
|
show_logs(session=session, tail=tail)
|
|
358
435
|
|
|
359
436
|
|
|
437
|
+
@cli.group()
|
|
438
|
+
def skills() -> None:
|
|
439
|
+
"""Manage the bundled Claude Code skills."""
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@skills.command(name="install")
|
|
443
|
+
@click.option("--force", is_flag=True, default=False, help="Replace skills that already exist in .claude/skills")
|
|
444
|
+
@click.pass_obj
|
|
445
|
+
def skills_install(state: CliState, force: bool) -> None:
|
|
446
|
+
"""Copy the bundled skills into .claude/skills/."""
|
|
447
|
+
for name, status in install_skills(state.root, force=force).items():
|
|
448
|
+
click.echo(f" {status} {name}")
|
|
449
|
+
|
|
450
|
+
|
|
360
451
|
main = cli
|
|
@@ -13,7 +13,7 @@ from pydantic import BaseModel
|
|
|
13
13
|
from pydantic_settings import BaseSettings
|
|
14
14
|
|
|
15
15
|
from captain_hook.llm import CodexBackend, LlmBackend, LlmBackends, TModel, TSpecialty
|
|
16
|
-
from captain_hook.prompt import
|
|
16
|
+
from captain_hook.prompt import Prompt
|
|
17
17
|
from captain_hook.session import SessionStore
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
@@ -119,7 +119,7 @@ class HookContext:
|
|
|
119
119
|
|
|
120
120
|
def call_llm(
|
|
121
121
|
self,
|
|
122
|
-
template: str |
|
|
122
|
+
template: str | Prompt,
|
|
123
123
|
*args: Any,
|
|
124
124
|
specialty: TSpecialty = "general",
|
|
125
125
|
model: TModel = "small",
|
|
@@ -129,7 +129,7 @@ class HookContext:
|
|
|
129
129
|
response_model: type[BaseModel] | None = None,
|
|
130
130
|
**kwargs: Any,
|
|
131
131
|
) -> str | BaseModel:
|
|
132
|
-
if isinstance(template,
|
|
132
|
+
if isinstance(template, Prompt):
|
|
133
133
|
prompt = str(template)
|
|
134
134
|
if transcript:
|
|
135
135
|
prompt = f"{self.transcript}\n\n<task>\n{prompt}\n</task>"
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from captain_hook.primitives.audit import audit as audit
|
|
4
|
-
from captain_hook.primitives.audit import session_id_for as session_id_for
|
|
5
4
|
from captain_hook.primitives.commands import block_command as block_command
|
|
6
5
|
from captain_hook.primitives.commands import warn_command as warn_command
|
|
7
|
-
from captain_hook.primitives.lint import diff_lint as diff_lint
|
|
8
6
|
from captain_hook.primitives.lint import lint as lint
|
|
9
7
|
from captain_hook.primitives.llm import (
|
|
10
8
|
GateVerdict as GateVerdict,
|
|
@@ -29,21 +27,3 @@ from captain_hook.primitives.llm import (
|
|
|
29
27
|
)
|
|
30
28
|
from captain_hook.primitives.nudge import gate as gate
|
|
31
29
|
from captain_hook.primitives.nudge import nudge as nudge
|
|
32
|
-
|
|
33
|
-
__all__ = [
|
|
34
|
-
"GateVerdict",
|
|
35
|
-
"NudgeVerdict",
|
|
36
|
-
"PromptCheckVerdict",
|
|
37
|
-
"audit",
|
|
38
|
-
"block_command",
|
|
39
|
-
"diff_lint",
|
|
40
|
-
"gate",
|
|
41
|
-
"lint",
|
|
42
|
-
"llm_evaluate",
|
|
43
|
-
"llm_gate",
|
|
44
|
-
"llm_nudge",
|
|
45
|
-
"nudge",
|
|
46
|
-
"prompt_check",
|
|
47
|
-
"session_id_for",
|
|
48
|
-
"warn_command",
|
|
49
|
-
]
|
|
@@ -12,7 +12,7 @@ from pydantic import BaseModel
|
|
|
12
12
|
from captain_hook import state
|
|
13
13
|
from captain_hook.app import on
|
|
14
14
|
from captain_hook.primitives.audit import session_id_for
|
|
15
|
-
from captain_hook.prompt import Prompt,
|
|
15
|
+
from captain_hook.prompt import Prompt, Prompt
|
|
16
16
|
from captain_hook.state import PrimitiveState, fired_this_turn, hook_name, record_fire
|
|
17
17
|
from captain_hook.types import (
|
|
18
18
|
Action,
|
|
@@ -335,7 +335,7 @@ def record_prompt_check_failure(
|
|
|
335
335
|
|
|
336
336
|
def prompt_check(
|
|
337
337
|
evt: BaseHookEvent,
|
|
338
|
-
template: str |
|
|
338
|
+
template: str | Prompt,
|
|
339
339
|
fmt: dict[str, Any] | None = None,
|
|
340
340
|
*,
|
|
341
341
|
prefix: str,
|
|
@@ -349,7 +349,7 @@ def prompt_check(
|
|
|
349
349
|
if include_reasoning:
|
|
350
350
|
reasoning = evt.ctx.t.recent(50).assistant_text() if hasattr(evt.ctx.t, "recent") else ""
|
|
351
351
|
|
|
352
|
-
base = template if isinstance(template,
|
|
352
|
+
base = template if isinstance(template, Prompt) else Prompt().system(template.format(**(fmt or {})))
|
|
353
353
|
built = base.context("agent_reasoning", reasoning or None)
|
|
354
354
|
prompt_str = str(built)
|
|
355
355
|
|
|
@@ -21,7 +21,7 @@ def _caller_dir() -> Path:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@dataclass(frozen=True, kw_only=True)
|
|
24
|
-
class
|
|
24
|
+
class Prompt:
|
|
25
25
|
"""Fluent builder for structured LLM prompts with system text, XML context sections, and a question.
|
|
26
26
|
|
|
27
27
|
Chain ``.system()``, ``.context(tag, content)``, and ``.ask()`` to build prompts.
|
|
@@ -32,38 +32,38 @@ class PromptMessage:
|
|
|
32
32
|
contexts: tuple[tuple[str, str], ...] = ()
|
|
33
33
|
ask_text: str = ""
|
|
34
34
|
|
|
35
|
-
def system(self, text: str) ->
|
|
36
|
-
return
|
|
35
|
+
def system(self, text: str) -> Prompt:
|
|
36
|
+
return Prompt(
|
|
37
37
|
system_text=dedent_text(text),
|
|
38
38
|
contexts=self.contexts,
|
|
39
39
|
ask_text=self.ask_text,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
def context(self, tag: str, content: str | None) ->
|
|
42
|
+
def context(self, tag: str, content: str | None) -> Prompt:
|
|
43
43
|
if content is None or not (normalized := dedent_text(content)):
|
|
44
44
|
return self
|
|
45
|
-
return
|
|
45
|
+
return Prompt(
|
|
46
46
|
system_text=self.system_text,
|
|
47
47
|
contexts=(*self.contexts, (tag, normalized)),
|
|
48
48
|
ask_text=self.ask_text,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
-
def ask(self, text: str) ->
|
|
52
|
-
return
|
|
51
|
+
def ask(self, text: str) -> Prompt:
|
|
52
|
+
return Prompt(
|
|
53
53
|
system_text=self.system_text,
|
|
54
54
|
contexts=self.contexts,
|
|
55
55
|
ask_text=dedent_text(text),
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
@classmethod
|
|
59
|
-
def from_template(cls, text: str, **vars: object) ->
|
|
59
|
+
def from_template(cls, text: str, **vars: object) -> Prompt:
|
|
60
60
|
try:
|
|
61
61
|
return cls(system_text=textwrap.dedent(text).strip().format_map(vars))
|
|
62
62
|
except KeyError as exc:
|
|
63
63
|
raise KeyError(f"template variable {exc.args[0]!r} not supplied") from exc
|
|
64
64
|
|
|
65
65
|
@classmethod
|
|
66
|
-
def load(cls, name: str, *, base: str | Path | None = None, **vars: object) ->
|
|
66
|
+
def load(cls, name: str, *, base: str | Path | None = None, **vars: object) -> Prompt:
|
|
67
67
|
"""Load a prompt from a ``.md`` file and render it via :meth:`from_template`.
|
|
68
68
|
|
|
69
69
|
Resolution searches directories in order, returning the first existing file:
|
|
@@ -77,7 +77,7 @@ class PromptMessage:
|
|
|
77
77
|
**vars: Template variables substituted into the file via ``str.format_map``.
|
|
78
78
|
|
|
79
79
|
Returns:
|
|
80
|
-
A :class:`
|
|
80
|
+
A :class:`Prompt` whose system text is the rendered file contents.
|
|
81
81
|
|
|
82
82
|
Raises:
|
|
83
83
|
FileNotFoundError: If no matching file exists in any searched directory.
|
|
@@ -100,4 +100,3 @@ class PromptMessage:
|
|
|
100
100
|
return "\n\n".join(parts)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
Prompt = PromptMessage
|
|
@@ -73,14 +73,3 @@ def resolve_signals(signals: Sequence[Signal | NlpSignal] | Signals | None) -> S
|
|
|
73
73
|
if isinstance(signals, Signals):
|
|
74
74
|
return signals
|
|
75
75
|
return Signals(patterns=list(signals), threshold=1)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
__all__ = [
|
|
79
|
-
"Signal",
|
|
80
|
-
"Signals",
|
|
81
|
-
"cite_message",
|
|
82
|
-
"extract_signal_context",
|
|
83
|
-
"resolve_signals",
|
|
84
|
-
"score_signals",
|
|
85
|
-
"transcript_texts",
|
|
86
|
-
]
|
|
@@ -8,9 +8,6 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from spacy.tokens import Doc, Span, Token
|
|
10
10
|
|
|
11
|
-
__all__ = ["Clause", "NlpSignal", "Phrase", "dep_related", "nlp_scan"]
|
|
12
|
-
|
|
13
|
-
|
|
14
11
|
@dataclass(frozen=True, slots=True, init=False)
|
|
15
12
|
class Phrase:
|
|
16
13
|
lemmas: tuple[str, ...]
|