capt-hook 3.5.0__tar.gz → 3.7.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.5.0 → capt_hook-3.7.0}/PKG-INFO +1 -1
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/cli.py +16 -12
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/manager.py +25 -9
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/cli.py +6 -6
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/SKILL.md +2 -2
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +1 -1
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +1 -1
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +7 -7
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +1 -1
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/SKILL.md +1 -1
- {capt_hook-3.5.0 → capt_hook-3.7.0}/pyproject.toml +1 -1
- {capt_hook-3.5.0 → capt_hook-3.7.0}/LICENSE +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/README.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/__main__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/app.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/command.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/conditions.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/context.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/decisions.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/events.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/file.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/llm/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/loader.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/log.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/capt-hook.toml +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/stewardship.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/prompt.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/py.typed +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/store.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/session.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/settings.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/state.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/types.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/tasks.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/tests/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/tests/helpers.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/types.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/utils.py +0 -0
|
@@ -155,21 +155,25 @@ def capt_hook_events(path: Path) -> set[str]:
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def sibling_settings(path: Path) -> Path:
|
|
159
|
+
return path.parent / ("settings.json" if path.name == "settings.local.json" else "settings.local.json")
|
|
160
|
+
|
|
161
|
+
|
|
158
162
|
def merge_settings(
|
|
159
163
|
hooks_dir: str, settings_path: Path, from_source: str = DIST_NAME
|
|
160
164
|
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
161
165
|
new_hooks: dict[str, list[dict[str, Any]]] = generate_settings(hooks_dir, from_source=from_source)["hooks"]
|
|
162
166
|
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
163
167
|
existing_hooks: dict[str, list[dict[str, Any]]] = existing.get("hooks") or {}
|
|
164
|
-
|
|
168
|
+
deferred = capt_hook_events(sibling_settings(settings_path))
|
|
165
169
|
|
|
166
170
|
summary: dict[str, str] = {}
|
|
167
171
|
merged_hooks: dict[str, list[dict[str, Any]]] = {}
|
|
168
172
|
for event in sorted(existing_hooks.keys() | new_hooks.keys()):
|
|
169
173
|
foreign = [g for g in existing_hooks.get(event, []) if not is_captain_hook_group(g)]
|
|
170
174
|
old_own = [g for g in existing_hooks.get(event, []) if is_captain_hook_group(g)]
|
|
171
|
-
fresh_own = [] if event in
|
|
172
|
-
if event in
|
|
175
|
+
fresh_own = [] if event in deferred else new_hooks.get(event, [])
|
|
176
|
+
if event in deferred and (old_own or new_hooks.get(event)):
|
|
173
177
|
summary[event] = "deferred"
|
|
174
178
|
elif old_own or fresh_own:
|
|
175
179
|
summary[event] = (
|
|
@@ -193,7 +197,7 @@ def write_settings(settings_path: Path, data: dict[str, Any]) -> None:
|
|
|
193
197
|
os.replace(tmp, settings_path)
|
|
194
198
|
|
|
195
199
|
|
|
196
|
-
def print_hook_summary(label: str, summary: dict[str, str]) -> None:
|
|
200
|
+
def print_hook_summary(label: str, summary: dict[str, str], deferred_to: str) -> None:
|
|
197
201
|
by_status: defaultdict[str, list[str]] = defaultdict(list)
|
|
198
202
|
for event, status in summary.items():
|
|
199
203
|
by_status[status].append(event)
|
|
@@ -209,15 +213,15 @@ def print_hook_summary(label: str, summary: dict[str, str]) -> None:
|
|
|
209
213
|
if unchanged := by_status["unchanged"]:
|
|
210
214
|
click.echo(f" unchanged: {', '.join(unchanged)} (already present)")
|
|
211
215
|
if deferred := by_status["deferred"]:
|
|
212
|
-
click.echo(f" deferred to
|
|
216
|
+
click.echo(f" deferred to {deferred_to}: {', '.join(deferred)}")
|
|
213
217
|
|
|
214
218
|
|
|
215
219
|
def regenerate_settings(state: CliState) -> None:
|
|
216
220
|
state.discover()
|
|
217
|
-
settings_path = state.root / ".claude" / "settings.
|
|
221
|
+
settings_path = state.root / ".claude" / "settings.json"
|
|
218
222
|
merged, summary = merge_settings(".claude/hooks", settings_path)
|
|
219
223
|
write_settings(settings_path, merged)
|
|
220
|
-
print_hook_summary(str(settings_path.relative_to(state.root)), summary)
|
|
224
|
+
print_hook_summary(str(settings_path.relative_to(state.root)), summary, sibling_settings(settings_path).name)
|
|
221
225
|
|
|
222
226
|
|
|
223
227
|
def settings_drift(root: Path) -> set[str]:
|
|
@@ -309,7 +313,7 @@ def init_project(root: Path, *, review: bool = True) -> None:
|
|
|
309
313
|
if not example.exists():
|
|
310
314
|
example.write_text(example_hook_source())
|
|
311
315
|
|
|
312
|
-
settings_path = root / ".claude" / "settings.
|
|
316
|
+
settings_path = root / ".claude" / "settings.json"
|
|
313
317
|
CliState(root=root, hooks=str(hooks_dir)).discover()
|
|
314
318
|
merged, summary = merge_settings(".claude/hooks", settings_path)
|
|
315
319
|
write_settings(settings_path, merged)
|
|
@@ -318,7 +322,7 @@ def init_project(root: Path, *, review: bool = True) -> None:
|
|
|
318
322
|
|
|
319
323
|
click.echo(f"Scaffolded {example.relative_to(root)} + {settings_path.relative_to(root)}.")
|
|
320
324
|
click.echo()
|
|
321
|
-
print_hook_summary(str(settings_path.relative_to(root)), summary)
|
|
325
|
+
print_hook_summary(str(settings_path.relative_to(root)), summary, sibling_settings(settings_path).name)
|
|
322
326
|
click.echo()
|
|
323
327
|
click.echo("Claude Code plugin:")
|
|
324
328
|
click.echo(f" + registered {PLUGIN_ID} in .claude/settings.json (skills install on folder-trust)")
|
|
@@ -476,15 +480,15 @@ def run(state: CliState, event: str, async_: bool) -> None:
|
|
|
476
480
|
)
|
|
477
481
|
@click.pass_obj
|
|
478
482
|
def register_hooks_cmd(state: CliState, hooks_dir: str, dry_run: bool, from_source: str) -> None:
|
|
479
|
-
"""Register captain-hook's event hooks into .claude/settings.
|
|
483
|
+
"""Register captain-hook's event hooks into .claude/settings.json."""
|
|
480
484
|
state.discover()
|
|
481
|
-
settings_path = state.root / ".claude" / "settings.
|
|
485
|
+
settings_path = state.root / ".claude" / "settings.json"
|
|
482
486
|
merged, summary = merge_settings(hooks_dir, settings_path, from_source=from_source)
|
|
483
487
|
if dry_run:
|
|
484
488
|
click.echo(json.dumps(merged, indent=2))
|
|
485
489
|
return
|
|
486
490
|
write_settings(settings_path, merged)
|
|
487
|
-
print_hook_summary(str(settings_path), summary)
|
|
491
|
+
print_hook_summary(str(settings_path), summary, sibling_settings(settings_path).name)
|
|
488
492
|
|
|
489
493
|
|
|
490
494
|
@cli.command()
|
|
@@ -27,6 +27,8 @@ from captain_hook import state
|
|
|
27
27
|
|
|
28
28
|
PACKS_TOML = "packs.toml"
|
|
29
29
|
PACK_MANIFEST = "capt-hook.toml"
|
|
30
|
+
# A pack's manifest may sit in .claude/ (preferred) or at the repo root.
|
|
31
|
+
MANIFEST_MEMBERS = (f".claude/{PACK_MANIFEST}", PACK_MANIFEST)
|
|
30
32
|
SHA_MARKER = ".sha"
|
|
31
33
|
SOURCE_RE = re.compile(r"^github:(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+?)(?:@(?P<ref>[\w./-]+))?$")
|
|
32
34
|
PACK_NAME_RE = re.compile(r"[a-z][a-z0-9-]*")
|
|
@@ -102,6 +104,16 @@ def packs_toml_path(root: Path) -> Path:
|
|
|
102
104
|
return root / ".claude" / "hooks" / PACKS_TOML
|
|
103
105
|
|
|
104
106
|
|
|
107
|
+
def manifest_in(root: Path) -> Path:
|
|
108
|
+
"""Return the pack manifest path under root, preferring .claude/capt-hook.toml.
|
|
109
|
+
|
|
110
|
+
Falls back to the repo-root location. The returned path may not exist (the
|
|
111
|
+
canonical missing location), so PackManifest.load still fails loudly.
|
|
112
|
+
"""
|
|
113
|
+
claude = root / ".claude" / PACK_MANIFEST
|
|
114
|
+
return claude if claude.is_file() else root / PACK_MANIFEST
|
|
115
|
+
|
|
116
|
+
|
|
105
117
|
def parse_entry(name: str, table: dict[str, Any]) -> PackEntry:
|
|
106
118
|
match table:
|
|
107
119
|
case {"source": source, "commit": commit}:
|
|
@@ -175,17 +187,18 @@ def strip_top_level(tf: tarfile.TarFile) -> Iterator[tarfile.TarInfo]:
|
|
|
175
187
|
yield member
|
|
176
188
|
|
|
177
189
|
|
|
178
|
-
def members_under(members: list[tarfile.TarInfo], hooks: str) -> Iterator[tarfile.TarInfo]:
|
|
190
|
+
def members_under(members: list[tarfile.TarInfo], hooks: str, manifest_path: str) -> Iterator[tarfile.TarInfo]:
|
|
179
191
|
"""Yield the manifest plus members within the pack's hooks dir.
|
|
180
192
|
|
|
181
193
|
hooks == "." (hooks beside the manifest) selects the whole tree; a real
|
|
182
194
|
subdir selects only the manifest and that subtree, so the cache holds just
|
|
183
|
-
what the loader imports.
|
|
195
|
+
what the loader imports. The manifest is included by its actual archive path
|
|
196
|
+
so a .claude/ manifest survives without dragging in the rest of .claude/.
|
|
184
197
|
"""
|
|
185
198
|
rel = hooks.strip("/")
|
|
186
199
|
prefix = "" if rel in ("", ".") else rel + "/"
|
|
187
200
|
for m in members:
|
|
188
|
-
if m.path ==
|
|
201
|
+
if m.path == manifest_path or not prefix or m.path == rel or m.path.startswith(prefix):
|
|
189
202
|
yield m
|
|
190
203
|
|
|
191
204
|
|
|
@@ -203,12 +216,15 @@ def fetch_commit(source: PackSource, sha: str) -> ResolvedPack:
|
|
|
203
216
|
shutil.rmtree(staging)
|
|
204
217
|
with tarfile.open(tarball) as tf:
|
|
205
218
|
members = list(strip_top_level(tf))
|
|
206
|
-
|
|
219
|
+
by_path = {m.path: m for m in members}
|
|
220
|
+
manifest_member = next((by_path[p] for p in MANIFEST_MEMBERS if p in by_path), None)
|
|
207
221
|
if manifest_member is None:
|
|
208
222
|
raise PackError(f"pack manifest {PACK_MANIFEST} missing in {source}")
|
|
209
223
|
tf.extract(manifest_member, staging, filter="data")
|
|
210
|
-
manifest = PackManifest.load(staging /
|
|
211
|
-
tf.extractall(
|
|
224
|
+
manifest = PackManifest.load(staging / manifest_member.path)
|
|
225
|
+
tf.extractall(
|
|
226
|
+
staging, members=list(members_under(members, manifest.hooks, manifest_member.path)), filter="data"
|
|
227
|
+
)
|
|
212
228
|
final = root / f"{manifest.name}@{sha}"
|
|
213
229
|
if final.exists():
|
|
214
230
|
shutil.rmtree(final)
|
|
@@ -225,20 +241,20 @@ def fetch_pack(source: PackSource) -> ResolvedPack:
|
|
|
225
241
|
|
|
226
242
|
def builtin_packs() -> dict[str, Path]:
|
|
227
243
|
base = Path(str(importlib.resources.files("captain_hook") / "packs"))
|
|
228
|
-
return {p.name: p for p in base.iterdir() if p.is_dir() and (p
|
|
244
|
+
return {p.name: p for p in base.iterdir() if p.is_dir() and manifest_in(p).is_file()}
|
|
229
245
|
|
|
230
246
|
|
|
231
247
|
def resolve_builtin(name: str) -> ResolvedPack:
|
|
232
248
|
if not (pack_dir := builtin_packs().get(name)):
|
|
233
249
|
raise PackError(f"unknown builtin pack {name!r}; available: {', '.join(sorted(builtin_packs())) or 'none'}")
|
|
234
|
-
manifest = PackManifest.load(pack_dir
|
|
250
|
+
manifest = PackManifest.load(manifest_in(pack_dir))
|
|
235
251
|
return ResolvedPack(BuiltinPack(name=name), manifest.hooks_dir(pack_dir), manifest)
|
|
236
252
|
|
|
237
253
|
|
|
238
254
|
def resolve_external(entry: ExternalPack) -> ResolvedPack | None:
|
|
239
255
|
if not (cached := find_cached(entry.name, entry.commit)):
|
|
240
256
|
return None
|
|
241
|
-
manifest = PackManifest.load(cached
|
|
257
|
+
manifest = PackManifest.load(manifest_in(cached))
|
|
242
258
|
return ResolvedPack(entry, manifest.hooks_dir(cached), manifest)
|
|
243
259
|
|
|
244
260
|
|
|
@@ -57,13 +57,13 @@ def review_wired(hooks: dict[str, Any]) -> bool:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def ensure_review_wiring(settings_path: Path) -> bool:
|
|
60
|
-
from captain_hook.cli import write_settings
|
|
60
|
+
from captain_hook.cli import sibling_settings, write_settings
|
|
61
61
|
|
|
62
62
|
existing: dict[str, Any] = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
sibling = sibling_settings(settings_path)
|
|
64
|
+
sibling_hooks: dict[str, Any] = (json.loads(sibling.read_text()).get("hooks") or {}) if sibling.exists() else {}
|
|
65
65
|
hooks: dict[str, Any] = existing.get("hooks") or {}
|
|
66
|
-
if review_wired(hooks) or review_wired(
|
|
66
|
+
if review_wired(hooks) or review_wired(sibling_hooks):
|
|
67
67
|
return False
|
|
68
68
|
group = {"hooks": [{"type": "command", "command": f"uvx {REVIEW_RUN_COMMAND}"}]}
|
|
69
69
|
write_settings(
|
|
@@ -126,8 +126,8 @@ def enable(state: CliState) -> None:
|
|
|
126
126
|
repo = current_repo(state.root)
|
|
127
127
|
watch_repo(repo)
|
|
128
128
|
register_marketplace(state.root)
|
|
129
|
-
wired = ensure_review_wiring(state.root / ".claude" / "settings.
|
|
130
|
-
click.echo(f"watching {repo}" + (" (SessionEnd hook wired into .claude/settings.
|
|
129
|
+
wired = ensure_review_wiring(state.root / ".claude" / "settings.json")
|
|
130
|
+
click.echo(f"watching {repo}" + (" (SessionEnd hook wired into .claude/settings.json)" if wired else ""))
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
@review.command()
|
|
@@ -9,7 +9,7 @@ allowed-tools: Read, Grep, Glob, Write, Edit, Bash(uvx capt-hook:*, capt-hook:*,
|
|
|
9
9
|
|
|
10
10
|
capt-hook is a declarative hook framework for Claude Code. Hooks are Python files in
|
|
11
11
|
`.claude/hooks/`, dispatched by `uvx capt-hook run <Event>` entries in
|
|
12
|
-
`.claude/settings.
|
|
12
|
+
`.claude/settings.json`. Each hook carries inline tests —
|
|
13
13
|
`tests={Input(...): Block() | Warn() | Allow()}` — run with `uvx capt-hook test`. This
|
|
14
14
|
skill turns **one durable correction** (the user's verbatim feedback plus the context it
|
|
15
15
|
fired in) into **one new hook file** `.claude/hooks/<slug>.py`. Full API:
|
|
@@ -111,7 +111,7 @@ hook.
|
|
|
111
111
|
### 5. Wire settings
|
|
112
112
|
|
|
113
113
|
Only after Step 4 is green, and only when the hook targets an event no existing
|
|
114
|
-
`.claude/settings.
|
|
114
|
+
`.claude/settings.json` entry dispatches:
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
117
|
uvx capt-hook register-hooks
|
{capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md
RENAMED
|
@@ -159,7 +159,7 @@ Glob caveat: patterns match the full relative path. `**/*.py` matches `src/main.
|
|
|
159
159
|
|---|---|
|
|
160
160
|
| `uvx capt-hook init` | Scaffold `.claude/hooks/example.py` + merge settings entries |
|
|
161
161
|
| `uvx capt-hook test [--json]` | Run all inline tests; exit 1 on failure; `--json` = one record per test |
|
|
162
|
-
| `uvx capt-hook register-hooks [--hooks-dir D] [--dry-run] [--from SRC]` | Merge captain-hook's hooks into `.claude/settings.
|
|
162
|
+
| `uvx capt-hook register-hooks [--hooks-dir D] [--dry-run] [--from SRC]` | Merge captain-hook's hooks into `.claude/settings.json` and write it (`--dry-run` prints without writing) |
|
|
163
163
|
| `uvx capt-hook run <Event> [--async]` | Dispatch one event (Claude Code calls this, not you) |
|
|
164
164
|
| `uvx capt-hook logs [--session S] [--tail N]` | View a recent capt-hook session log |
|
|
165
165
|
|
{capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md
RENAMED
|
@@ -44,7 +44,7 @@ broken hook.
|
|
|
44
44
|
|
|
45
45
|
Wire only commands proven to run: execute the exact settings command by hand first, and
|
|
46
46
|
prefer `uvx capt-hook register-hooks` (which writes known-good entries) over editing
|
|
47
|
-
`.claude/settings.
|
|
47
|
+
`.claude/settings.json` manually.
|
|
48
48
|
|
|
49
49
|
## 4. `uvx capt-hook test` green BEFORE wiring — always
|
|
50
50
|
|
|
@@ -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. 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.
|
|
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.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
|
---
|
|
@@ -9,7 +9,7 @@ allowed-tools: Read, Grep, Glob, AskUserQuestion, Write, Edit, Bash(uvx capt-hoo
|
|
|
9
9
|
|
|
10
10
|
capt-hook is a declarative hook framework for Claude Code. Hooks are Python files in
|
|
11
11
|
`.claude/hooks/`, dispatched by `uvx capt-hook run <Event>` entries in
|
|
12
|
-
`.claude/settings.
|
|
12
|
+
`.claude/settings.json`. Each hook carries inline tests —
|
|
13
13
|
`tests={Input(...): Block() | Warn() | Allow()}` — run with `uvx capt-hook test`. Hooks are
|
|
14
14
|
always Python regardless of the target repo's language: conditions like `Command` and
|
|
15
15
|
`FilePath` are language-agnostic; only AST `lint` rules are Python-specific. The full
|
|
@@ -51,11 +51,11 @@ grep -lq 'capt-hook' .claude/settings.json 2>/dev/null && echo COMMITTED || echo
|
|
|
51
51
|
|
|
52
52
|
Then scaffold up front, so the framework and the session reviewer are live before you propose
|
|
53
53
|
anything. Run `uvx capt-hook init` in every repo. It scaffolds `.claude/hooks/`,
|
|
54
|
-
wires `.claude/settings.
|
|
54
|
+
wires `.claude/settings.json`, installs the skills, and **enables the session reviewer**
|
|
55
55
|
(watching this repo; it mines ended sessions and opens hook PRs — `uvx capt-hook review disable`
|
|
56
|
-
to stop).
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
to stop). When `.claude/settings.local.json` already runs `uvx capt-hook run …` for some events
|
|
57
|
+
(a per-machine setup), `init` defers those events to the local file instead of duplicating them
|
|
58
|
+
into the committed settings. It prints `deferred to settings.local.json: …` and never double-fires.
|
|
59
59
|
|
|
60
60
|
Read `.claude/settings.local.json` and `.claude/settings.json`. If capt-hook hooks already exist,
|
|
61
61
|
switch to **additive mode**: never overwrite existing hook files; new categories go in new files,
|
|
@@ -154,7 +154,7 @@ after scaffolding). Run:
|
|
|
154
154
|
uvx capt-hook register-hooks
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
`register-hooks` writes `.claude/settings.
|
|
157
|
+
`register-hooks` writes `.claude/settings.json` directly, merging non-destructively: it
|
|
158
158
|
preserves every non-captain-hook entry, refreshes captain-hook's own, and drops entries for
|
|
159
159
|
events you no longer subscribe to. Add `--dry-run` to print the merged JSON without writing.
|
|
160
160
|
|
{capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/references/review-cli.md
RENAMED
|
@@ -25,7 +25,7 @@ defaults to the process cwd. This is what spawned you; do not recurse into it.
|
|
|
25
25
|
### `review enable` / `review disable`
|
|
26
26
|
|
|
27
27
|
`enable` marks the current repo watched and wires the SessionEnd hook into
|
|
28
|
-
`.claude/settings.
|
|
28
|
+
`.claude/settings.json` (idempotent). `disable` stops watching; candidates stay
|
|
29
29
|
recorded but never become eligible.
|
|
30
30
|
|
|
31
31
|
### `review scan [--transcript <file>]... [--dir <dir>]...`
|
|
@@ -166,7 +166,7 @@ and every `Input` runs through the *whole* styleguide. A failing test usually me
|
|
|
166
166
|
input trips a sibling rule — shrink it to a single construct that trips exactly one rule.
|
|
167
167
|
|
|
168
168
|
If `style_llm.py` added hooks on new events (e.g. a `Stop`-targeted `llm_nudge`), run
|
|
169
|
-
`uvx capt-hook register-hooks` (it merges non-destructively into `.claude/settings.
|
|
169
|
+
`uvx capt-hook register-hooks` (it merges non-destructively into `.claude/settings.json`
|
|
170
170
|
and writes it).
|
|
171
171
|
|
|
172
172
|
### 8. Enforcement report
|
|
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.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.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
|