capt-hook 3.2.0__tar.gz → 3.3.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.2.0 → capt_hook-3.3.0}/PKG-INFO +21 -15
- {capt_hook-3.2.0 → capt_hook-3.3.0}/README.md +20 -14
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/cli.py +40 -27
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/manager.py +28 -5
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/cli.py +16 -9
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/settings.py +1 -1
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +28 -19
- {capt_hook-3.2.0 → capt_hook-3.3.0}/pyproject.toml +1 -1
- {capt_hook-3.2.0 → capt_hook-3.3.0}/LICENSE +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/__main__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/app.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/command.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/conditions.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/context.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/decisions.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/events.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/file.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/llm/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/loader.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/log.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/capt-hook.toml +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/stewardship.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/prompt.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/py.typed +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/store.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/session.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/settings.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/state.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/style/types.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/tasks.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/tests/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/tests/helpers.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/types.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-3.2.0 → capt_hook-3.3.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.
|
|
3
|
+
Version: 3.3.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
|
|
@@ -55,23 +55,28 @@ Description-Content-Type: text/markdown
|
|
|
55
55
|
|
|
56
56
|
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.
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## Quickstart
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
No install step — everything runs through [uvx](https://docs.astral.sh/uv/). Pick a front door:
|
|
61
|
+
|
|
62
|
+
**From your terminal:**
|
|
61
63
|
|
|
62
64
|
```bash
|
|
63
65
|
uvx capt-hook init
|
|
64
66
|
```
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
**From inside Claude Code** — install the plugin, then ask Claude to set it up:
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
```
|
|
71
|
+
/plugin marketplace add yasyf/captain-hook
|
|
72
|
+
/plugin install captain-hook@captain-hook
|
|
73
|
+
```
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
> set up captain hook
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
Either path lands in the same place: `.claude/hooks/` scaffolded, Claude Code's settings wired, the bundled skills installed, and the [session reviewer](#session-reviewer) watching this repo. `uvx` fetches captain-hook into a throwaway environment, so it never enters your `pyproject.toml` — and every command below works the same way once you prefix it with `uvx`.
|
|
78
|
+
|
|
79
|
+
## Your first hook
|
|
75
80
|
|
|
76
81
|
A hook is declarative Python with an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at.
|
|
77
82
|
|
|
@@ -117,14 +122,15 @@ uvx capt-hook test
|
|
|
117
122
|
|
|
118
123
|
`init` already wired Claude Code's settings. Each event runs `uvx capt-hook run <Event>`, with the event JSON arriving on stdin and the verdict written to stdout. Re-run `uvx capt-hook register-hooks` only after you add hooks on a new event; it writes `.claude/settings.local.json` for you.
|
|
119
124
|
|
|
120
|
-
##
|
|
125
|
+
## Session reviewer
|
|
121
126
|
|
|
122
|
-
|
|
127
|
+
`init` also turns on the **session reviewer**. When a Claude Code session ends, it mines the transcript for the durable corrections you gave and the hooks that misfired, judges each one, and — once a pattern clears its thresholds — opens a pull request that adds a new hook or fixes the one that misfired. You review the PR like any other.
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`, or skip it at setup with `uvx capt-hook init --no-review`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` tuning knobs.
|
|
130
|
+
|
|
131
|
+
## Agent Skills
|
|
132
|
+
|
|
133
|
+
captain-hook ships two [Agent Skills](https://yasyf.github.io/captain-hook/docs/getting-started/skills.html) so you don't have to write hooks by hand. `bootstrapping-hooks` surveys your repo's docs, CI, and git history and proposes gates and nudges; `translating-styleguides` turns a STYLEGUIDE.md into enforced rules. Both land in `.claude/skills/` via `init` and ship as the plugin in the [Quickstart](#quickstart) — ask Claude to "set up captain hook" and `bootstrapping-hooks` takes it from there.
|
|
128
134
|
|
|
129
135
|
## What this solves
|
|
130
136
|
|
|
@@ -9,23 +9,28 @@
|
|
|
9
9
|
|
|
10
10
|
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.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Quickstart
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
No install step — everything runs through [uvx](https://docs.astral.sh/uv/). Pick a front door:
|
|
15
|
+
|
|
16
|
+
**From your terminal:**
|
|
15
17
|
|
|
16
18
|
```bash
|
|
17
19
|
uvx capt-hook init
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
**From inside Claude Code** — install the plugin, then ask Claude to set it up:
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
```
|
|
25
|
+
/plugin marketplace add yasyf/captain-hook
|
|
26
|
+
/plugin install captain-hook@captain-hook
|
|
27
|
+
```
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
> set up captain hook
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
Either path lands in the same place: `.claude/hooks/` scaffolded, Claude Code's settings wired, the bundled skills installed, and the [session reviewer](#session-reviewer) watching this repo. `uvx` fetches captain-hook into a throwaway environment, so it never enters your `pyproject.toml` — and every command below works the same way once you prefix it with `uvx`.
|
|
32
|
+
|
|
33
|
+
## Your first hook
|
|
29
34
|
|
|
30
35
|
A hook is declarative Python with an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at.
|
|
31
36
|
|
|
@@ -71,14 +76,15 @@ uvx capt-hook test
|
|
|
71
76
|
|
|
72
77
|
`init` already wired Claude Code's settings. Each event runs `uvx capt-hook run <Event>`, with the event JSON arriving on stdin and the verdict written to stdout. Re-run `uvx capt-hook register-hooks` only after you add hooks on a new event; it writes `.claude/settings.local.json` for you.
|
|
73
78
|
|
|
74
|
-
##
|
|
79
|
+
## Session reviewer
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
`init` also turns on the **session reviewer**. When a Claude Code session ends, it mines the transcript for the durable corrections you gave and the hooks that misfired, judges each one, and — once a pattern clears its thresholds — opens a pull request that adds a new hook or fixes the one that misfired. You review the PR like any other.
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`, or skip it at setup with `uvx capt-hook init --no-review`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` tuning knobs.
|
|
84
|
+
|
|
85
|
+
## Agent Skills
|
|
86
|
+
|
|
87
|
+
captain-hook ships two [Agent Skills](https://yasyf.github.io/captain-hook/docs/getting-started/skills.html) so you don't have to write hooks by hand. `bootstrapping-hooks` surveys your repo's docs, CI, and git history and proposes gates and nudges; `translating-styleguides` turns a STYLEGUIDE.md into enforced rules. Both land in `.claude/skills/` via `init` and ship as the plugin in the [Quickstart](#quickstart) — ask Claude to "set up captain hook" and `bootstrapping-hooks` takes it from there.
|
|
82
88
|
|
|
83
89
|
## What this solves
|
|
84
90
|
|
|
@@ -196,17 +196,17 @@ def print_hook_summary(label: str, summary: dict[str, str]) -> None:
|
|
|
196
196
|
by_status: defaultdict[str, list[str]] = defaultdict(list)
|
|
197
197
|
for event, status in summary.items():
|
|
198
198
|
by_status[status].append(event)
|
|
199
|
-
|
|
199
|
+
click.echo(f"{label}:")
|
|
200
200
|
if not summary:
|
|
201
|
-
|
|
201
|
+
click.echo(" no hook entries")
|
|
202
202
|
for event in by_status["added"]:
|
|
203
|
-
|
|
203
|
+
click.echo(f" + added {event}")
|
|
204
204
|
for event in by_status["updated"]:
|
|
205
|
-
|
|
205
|
+
click.echo(f" ~ updated {event}")
|
|
206
206
|
for event in by_status["removed"]:
|
|
207
|
-
|
|
207
|
+
click.echo(f" - removed {event}")
|
|
208
208
|
if unchanged := by_status["unchanged"]:
|
|
209
|
-
|
|
209
|
+
click.echo(f" unchanged: {', '.join(unchanged)} (already present)")
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
def regenerate_settings(state: CliState) -> None:
|
|
@@ -303,13 +303,15 @@ def run_event(
|
|
|
303
303
|
print(json.dumps(output))
|
|
304
304
|
|
|
305
305
|
|
|
306
|
-
def init_project(root: Path) -> None:
|
|
306
|
+
def init_project(root: Path, *, review: bool = True) -> None:
|
|
307
|
+
from captain_hook.review.cli import watch_repo
|
|
308
|
+
from captain_hook.review.repo import repo_key
|
|
309
|
+
|
|
307
310
|
hooks_dir = root / ".claude" / "hooks"
|
|
308
311
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
309
312
|
|
|
310
313
|
example = hooks_dir / "example.py"
|
|
311
|
-
|
|
312
|
-
if example_created:
|
|
314
|
+
if not example.exists():
|
|
313
315
|
example.write_text(example_hook_source())
|
|
314
316
|
|
|
315
317
|
settings_path = root / ".claude" / "settings.local.json"
|
|
@@ -319,23 +321,33 @@ def init_project(root: Path) -> None:
|
|
|
319
321
|
|
|
320
322
|
skills_summary = install_skills(root)
|
|
321
323
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
click.echo(f"Scaffolded {example.relative_to(root)} + {settings_path.relative_to(root)}.")
|
|
325
|
+
click.echo()
|
|
324
326
|
print_hook_summary(str(settings_path.relative_to(root)), summary)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
+
click.echo()
|
|
328
|
+
click.echo(".claude/skills/:")
|
|
327
329
|
for name in (n for n, status in skills_summary.items() if status == "installed"):
|
|
328
|
-
|
|
330
|
+
click.echo(f" + installed {name}")
|
|
329
331
|
if skipped := [n for n, status in skills_summary.items() if status == "skipped"]:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
332
|
+
click.echo(f" unchanged: {', '.join(skipped)} (already present; capt-hook skills install --force to refresh)")
|
|
333
|
+
click.echo()
|
|
334
|
+
match (review, repo_key(root)):
|
|
335
|
+
case (False, _):
|
|
336
|
+
click.echo("Session reviewer: skipped (--no-review) — `uvx capt-hook review enable` to turn it on later.")
|
|
337
|
+
case (True, None):
|
|
338
|
+
click.echo("Session reviewer: needs a git repo with a remote — `uvx capt-hook review enable` once it has one.")
|
|
339
|
+
case (True, repo):
|
|
340
|
+
watch_repo(repo)
|
|
341
|
+
click.echo(f"Session reviewer: watching {repo} — mines your ended sessions and opens hook PRs automatically.")
|
|
342
|
+
click.echo(" Stop anytime with `uvx capt-hook review disable`.")
|
|
343
|
+
click.echo()
|
|
344
|
+
click.echo("Next:")
|
|
345
|
+
click.echo(" 1. Read the quickstart: https://yasyf.github.io/captain-hook/")
|
|
346
|
+
click.echo(" 2. Edit example.py or add new files under .claude/hooks/")
|
|
347
|
+
click.echo(" 3. uvx capt-hook test # verify inline tests")
|
|
348
|
+
click.echo(" 4. uvx capt-hook register-hooks # re-register after adding events")
|
|
349
|
+
click.echo(' 5. Ask Claude "set up captain hook" # mine guardrails from this repo (the bootstrapping-hooks skill)')
|
|
350
|
+
click.echo()
|
|
339
351
|
maybe_launch_bootstrap(root)
|
|
340
352
|
|
|
341
353
|
|
|
@@ -493,10 +505,11 @@ def test(state: CliState, json_output: bool) -> None:
|
|
|
493
505
|
|
|
494
506
|
|
|
495
507
|
@cli.command()
|
|
508
|
+
@click.option("--no-review", is_flag=True, default=False, help="Skip enabling the SessionEnd session reviewer for this repo")
|
|
496
509
|
@click.pass_obj
|
|
497
|
-
def init(state: CliState) -> None:
|
|
498
|
-
"""Scaffold the hooks directory, install bundled skills, and
|
|
499
|
-
init_project(state.root)
|
|
510
|
+
def init(state: CliState, no_review: bool) -> None:
|
|
511
|
+
"""Scaffold the hooks directory, install bundled skills, wire settings, and enable the session reviewer."""
|
|
512
|
+
init_project(state.root, review=not no_review)
|
|
500
513
|
|
|
501
514
|
|
|
502
515
|
@cli.command()
|
|
@@ -537,9 +550,9 @@ def pack_add(state: CliState, target: str) -> None:
|
|
|
537
550
|
if target in manager.builtin_packs()
|
|
538
551
|
else manager.fetch_pack(manager.PackSource.parse(target)).entry
|
|
539
552
|
)
|
|
540
|
-
manager.upsert_entry(manager.packs_toml_path(state.root), entry)
|
|
541
553
|
except manager.PackError as e:
|
|
542
554
|
raise click.ClickException(str(e)) from e
|
|
555
|
+
manager.upsert_entry(manager.packs_toml_path(state.root), entry)
|
|
543
556
|
click.echo(f" added {entry.name}")
|
|
544
557
|
regenerate_settings(state)
|
|
545
558
|
|
|
@@ -170,11 +170,25 @@ def resolve_commit(source: PackSource) -> str:
|
|
|
170
170
|
|
|
171
171
|
def strip_top_level(tf: tarfile.TarFile) -> Iterator[tarfile.TarInfo]:
|
|
172
172
|
for member in tf.getmembers():
|
|
173
|
-
if
|
|
173
|
+
if tail := member.name.partition("/")[2]:
|
|
174
174
|
member.path = tail
|
|
175
175
|
yield member
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def members_under(members: list[tarfile.TarInfo], hooks: str) -> Iterator[tarfile.TarInfo]:
|
|
179
|
+
"""Yield the manifest plus members within the pack's hooks dir.
|
|
180
|
+
|
|
181
|
+
hooks == "." (hooks beside the manifest) selects the whole tree; a real
|
|
182
|
+
subdir selects only the manifest and that subtree, so the cache holds just
|
|
183
|
+
what the loader imports.
|
|
184
|
+
"""
|
|
185
|
+
rel = hooks.strip("/")
|
|
186
|
+
prefix = "" if rel in ("", ".") else rel + "/"
|
|
187
|
+
for m in members:
|
|
188
|
+
if m.path == PACK_MANIFEST or not prefix or m.path == rel or m.path.startswith(prefix):
|
|
189
|
+
yield m
|
|
190
|
+
|
|
191
|
+
|
|
178
192
|
def find_cached(name: str, sha: str) -> Path | None:
|
|
179
193
|
return d if (d := packs_cache_root() / f"{name}@{sha}").is_dir() and (d / SHA_MARKER).is_file() else None
|
|
180
194
|
|
|
@@ -188,14 +202,21 @@ def fetch_commit(source: PackSource, sha: str) -> ResolvedPack:
|
|
|
188
202
|
if staging.exists():
|
|
189
203
|
shutil.rmtree(staging)
|
|
190
204
|
with tarfile.open(tarball) as tf:
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
members = list(strip_top_level(tf))
|
|
206
|
+
manifest_member = next((m for m in members if m.path == PACK_MANIFEST), None)
|
|
207
|
+
if manifest_member is None:
|
|
208
|
+
raise PackError(f"pack manifest {PACK_MANIFEST} missing in {source}")
|
|
209
|
+
tf.extract(manifest_member, staging, filter="data")
|
|
210
|
+
manifest = PackManifest.load(staging / PACK_MANIFEST)
|
|
211
|
+
tf.extractall(staging, members=list(members_under(members, manifest.hooks)), filter="data")
|
|
193
212
|
final = root / f"{manifest.name}@{sha}"
|
|
194
213
|
if final.exists():
|
|
195
214
|
shutil.rmtree(final)
|
|
196
215
|
os.replace(staging, final)
|
|
197
216
|
(final / SHA_MARKER).write_text(sha)
|
|
198
|
-
return ResolvedPack(
|
|
217
|
+
return ResolvedPack(
|
|
218
|
+
ExternalPack(name=manifest.name, source=source, commit=sha), manifest.hooks_dir(final), manifest
|
|
219
|
+
)
|
|
199
220
|
|
|
200
221
|
|
|
201
222
|
def fetch_pack(source: PackSource) -> ResolvedPack:
|
|
@@ -228,6 +249,8 @@ def resolve_enabled_packs(root: Path) -> tuple[list[ResolvedPack], list[str]]:
|
|
|
228
249
|
match entry:
|
|
229
250
|
case BuiltinPack(name=name):
|
|
230
251
|
resolved.append(resolve_builtin(name))
|
|
252
|
+
case ExternalPack() as ext if found := resolve_external(ext):
|
|
253
|
+
resolved.append(found)
|
|
231
254
|
case ExternalPack() as ext:
|
|
232
|
-
|
|
255
|
+
missing.append(ext.name)
|
|
233
256
|
return resolved, missing
|
|
@@ -71,6 +71,18 @@ def ensure_review_wiring(settings_path: Path) -> bool:
|
|
|
71
71
|
return True
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def watch_repo(repo: RepoKey) -> None:
|
|
75
|
+
"""Flip the global watching bit for ``repo`` — the single persistence path shared by ``init`` and ``review enable``."""
|
|
76
|
+
from captain_hook.review.settings import ReviewSettings
|
|
77
|
+
from captain_hook.review.store import ReviewStore
|
|
78
|
+
|
|
79
|
+
async def go() -> None:
|
|
80
|
+
async with await ReviewStore.open(ReviewSettings().db_path) as store:
|
|
81
|
+
await store.enable(repo)
|
|
82
|
+
|
|
83
|
+
asyncio.run(go())
|
|
84
|
+
|
|
85
|
+
|
|
74
86
|
def candidate_line(row: dict[str, object]) -> str:
|
|
75
87
|
text = str(row["sample_text"] or "").replace("\n", " ")
|
|
76
88
|
return (
|
|
@@ -106,17 +118,12 @@ def spawn(transcript: Path, cwd: str | None) -> None:
|
|
|
106
118
|
@review.command()
|
|
107
119
|
@click.pass_obj
|
|
108
120
|
def enable(state: CliState) -> None:
|
|
109
|
-
"""Watch the current repo and wire the SessionEnd
|
|
110
|
-
from captain_hook.
|
|
111
|
-
from captain_hook.review.store import ReviewStore
|
|
121
|
+
"""Watch the current repo, install the reviewer's skills, and wire the SessionEnd hook."""
|
|
122
|
+
from captain_hook.cli import install_skills
|
|
112
123
|
|
|
113
124
|
repo = current_repo(state.root)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
async with await ReviewStore.open(ReviewSettings().db_path) as store:
|
|
117
|
-
await store.enable(repo)
|
|
118
|
-
|
|
119
|
-
asyncio.run(go())
|
|
125
|
+
watch_repo(repo)
|
|
126
|
+
install_skills(state.root)
|
|
120
127
|
wired = ensure_review_wiring(state.root / ".claude" / "settings.local.json")
|
|
121
128
|
click.echo(f"watching {repo}" + (" (SessionEnd hook wired into .claude/settings.local.json)" if wired else ""))
|
|
122
129
|
|
|
@@ -36,7 +36,7 @@ class ReviewSettings(HooksSettings):
|
|
|
36
36
|
min_days_fix: int = 0
|
|
37
37
|
min_confidence_fix: Confidence = MEDIUM
|
|
38
38
|
min_confidence_fix_single: Confidence = VERY_HIGH
|
|
39
|
-
judge_tier: TModel = "
|
|
39
|
+
judge_tier: TModel = "medium"
|
|
40
40
|
judge_concurrency: int = 8
|
|
41
41
|
judge_timeout: int = 180
|
|
42
42
|
min_judge_confidence: float = 0.6
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bootstrapping-hooks
|
|
3
|
-
description: Surveys a repository and sets up captain-hook (capt-hook) guardrails for Claude Code — blocking gates, advisory nudges, command blocks, and test-integrity checks mined from the repo's own docs, CI workflows, lint configs, and git history.
|
|
3
|
+
description: Surveys a repository and sets up captain-hook (capt-hook) guardrails for Claude Code — blocking gates, advisory nudges, command blocks, and test-integrity checks mined from the repo's own docs, CI workflows, lint configs, and git history. Scaffolds the framework and enables the session reviewer up front (Step 1), then proposes categorized candidates for user approval before writing anything, then writes .claude/hooks/*.py with inline tests, verifies with capt-hook test, and wires .claude/settings.local.json. Use when the user asks to "set up captain hook", "set up capt-hook", "set up hooks", "bootstrap capt-hook", "add guardrails", "enforce our conventions with hooks", "protect this repo", or "make Claude follow CONTRIBUTING.md".
|
|
4
4
|
argument-hint: "[repo path] (defaults to current project)"
|
|
5
5
|
allowed-tools: Read, Grep, Glob, AskUserQuestion, Write, Edit, Bash(uvx capt-hook:*, capt-hook:*, git log:*, git diff:*, ls:*, find:*)
|
|
6
6
|
---
|
|
@@ -29,28 +29,41 @@ Copy this checklist into your response and check off steps as you complete them:
|
|
|
29
29
|
|
|
30
30
|
```
|
|
31
31
|
Bootstrap Progress:
|
|
32
|
-
- [ ] Step 1: Locate +
|
|
32
|
+
- [ ] Step 1: Locate + scaffold (init or review enable) + pre-flight
|
|
33
33
|
- [ ] Step 2: Survey the repo (docs, CI, lint configs, git log)
|
|
34
34
|
- [ ] Step 3: Mine candidates onto the taxonomy
|
|
35
35
|
- [ ] Step 4: Propose via AskUserQuestion — nothing written before approval
|
|
36
|
-
- [ ] Step 5:
|
|
36
|
+
- [ ] Step 5: Clear the demo example.py (scaffolding ran in Step 1)
|
|
37
37
|
- [ ] Step 6: Draft approved hooks via authoring-hooks, one file per category
|
|
38
38
|
- [ ] Step 7: Verify (uvx capt-hook test, fix until green)
|
|
39
39
|
- [ ] Step 8: Wire settings (register-hooks if new events)
|
|
40
40
|
- [ ] Step 9: Final report (table + declined list)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
### 1. Locate + pre-flight
|
|
43
|
+
### 1. Locate + scaffold + pre-flight
|
|
44
44
|
|
|
45
|
-
Resolve the target repo (argument path, else current project).
|
|
45
|
+
Resolve the target repo (argument path, else current project). Inspect what's already wired:
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
ls .claude/hooks/ 2>/dev/null
|
|
49
|
+
grep -lq 'capt-hook' .claude/settings.json 2>/dev/null && echo COMMITTED || echo FRESH
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
Then scaffold up front, so the framework and the session reviewer are live before you propose
|
|
53
|
+
anything — pick the command by what you found:
|
|
54
|
+
|
|
55
|
+
- **FRESH** (no committed capt-hook wiring) — run `uvx capt-hook init`. It scaffolds
|
|
56
|
+
`.claude/hooks/`, wires `.claude/settings.local.json`, installs the skills, and **enables the
|
|
57
|
+
session reviewer** (watching this repo; it mines ended sessions and opens hook PRs —
|
|
58
|
+
`uvx capt-hook review disable` to stop).
|
|
59
|
+
- **COMMITTED** (a checked-in `.claude/settings.json` already runs `uvx capt-hook run …`) — do
|
|
60
|
+
**not** run `init`; it would duplicate those hooks into `settings.local.json` and double-fire.
|
|
61
|
+
Run `uvx capt-hook review enable` instead — it installs the reviewer skills and arms the session
|
|
62
|
+
reviewer without touching the committed event hooks.
|
|
63
|
+
|
|
64
|
+
Read `.claude/settings.local.json` and `.claude/settings.json`. If capt-hook hooks already exist,
|
|
65
|
+
switch to **additive mode**: never overwrite existing hook files; new categories go in new files,
|
|
66
|
+
and the Step 4 menu only offers candidates not already covered.
|
|
54
67
|
|
|
55
68
|
### 2. Survey the repo
|
|
56
69
|
|
|
@@ -99,17 +112,11 @@ and the proposed primitive + severity. Then one final severity question:
|
|
|
99
112
|
If a styleguide-like markdown was found, category E is a single option: **"Translate `<file>`
|
|
100
113
|
into enforced style rules (runs the translating-styleguides skill)"**.
|
|
101
114
|
|
|
102
|
-
### 5.
|
|
103
|
-
|
|
104
|
-
Run:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
uvx capt-hook init
|
|
108
|
-
```
|
|
115
|
+
### 5. Clear the demo example.py
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
Scaffolding already ran in Step 1. If that `init` created the demo `.claude/hooks/example.py`,
|
|
118
|
+
delete it once you've drafted the real hooks (Step 6) — the approved hooks replace it. (In
|
|
119
|
+
**COMMITTED** repos `review enable` writes no `example.py`, so there's nothing to clear.)
|
|
113
120
|
|
|
114
121
|
### 6. Write hooks
|
|
115
122
|
|
|
@@ -168,7 +175,9 @@ Declined: <candidates the user rejected, with their source quotes>
|
|
|
168
175
|
```
|
|
169
176
|
|
|
170
177
|
Close with next steps: `uvx capt-hook logs --tail 50` to inspect live firings, and tune
|
|
171
|
-
`max_fires` on any hook that nags.
|
|
178
|
+
`max_fires` on any hook that nags. Note that Step 1 also armed the **session reviewer** — it now
|
|
179
|
+
watches this repo, mines your ended sessions for durable corrections, and opens hook PRs
|
|
180
|
+
automatically; `uvx capt-hook review disable` turns it off.
|
|
172
181
|
|
|
173
182
|
## Worked mini-example
|
|
174
183
|
|
|
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.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md
RENAMED
|
File without changes
|
{capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-3.2.0 → capt_hook-3.3.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md
RENAMED
|
File without changes
|
{capt_hook-3.2.0 → capt_hook-3.3.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
|