capt-hook 3.4.0__tar.gz → 3.6.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.
Files changed (91) hide show
  1. {capt_hook-3.4.0 → capt_hook-3.6.0}/PKG-INFO +2 -2
  2. {capt_hook-3.4.0 → capt_hook-3.6.0}/README.md +1 -1
  3. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/cli.py +23 -39
  4. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/manager.py +25 -9
  5. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/cli.py +3 -3
  6. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/pipeline.py +11 -2
  7. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/SKILL.md +1 -1
  8. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +1 -1
  9. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +2 -2
  10. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/scanning-sessions/SKILL.md +3 -3
  11. {capt_hook-3.4.0 → capt_hook-3.6.0}/pyproject.toml +1 -1
  12. {capt_hook-3.4.0 → capt_hook-3.6.0}/LICENSE +0 -0
  13. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  14. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/__init__.py +0 -0
  15. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/__main__.py +0 -0
  16. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/app.py +0 -0
  17. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/classifiers/__init__.py +0 -0
  18. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/classifiers/conductor.py +0 -0
  19. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/classifiers/droid.py +0 -0
  20. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/classifiers/native.py +0 -0
  21. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/command.py +0 -0
  22. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/conditions.py +0 -0
  23. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/context.py +0 -0
  24. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/decisions.py +0 -0
  25. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/dispatch.py +0 -0
  26. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/events.py +0 -0
  27. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/file.py +0 -0
  28. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/llm/__init__.py +0 -0
  29. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/loader.py +0 -0
  30. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/log.py +0 -0
  31. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/__init__.py +0 -0
  32. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/capt-hook.toml +0 -0
  33. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/commands.py +0 -0
  34. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/docs.py +0 -0
  35. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/plans.py +0 -0
  36. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/prompts.py +0 -0
  37. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/review.py +0 -0
  38. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/stewardship.py +0 -0
  39. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/general/tasks.py +0 -0
  40. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  41. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/python/style.py +0 -0
  42. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/python/testing.py +0 -0
  43. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/packs/python/toolchain.py +0 -0
  44. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/primitives/__init__.py +0 -0
  45. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/primitives/commands.py +0 -0
  46. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/primitives/lint.py +0 -0
  47. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/primitives/llm.py +0 -0
  48. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/primitives/nudge.py +0 -0
  49. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/primitives/workflow.py +0 -0
  50. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/prompt.py +0 -0
  51. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/py.typed +0 -0
  52. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/__init__.py +0 -0
  53. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/dashboard.py +0 -0
  54. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/fix.py +0 -0
  55. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/formats.py +0 -0
  56. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/judge.py +0 -0
  57. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/repo.py +0 -0
  58. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/scan.py +0 -0
  59. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/settings.py +0 -0
  60. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/store.py +0 -0
  61. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/review/sync.py +0 -0
  62. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/session.py +0 -0
  63. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/settings.py +0 -0
  64. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/signals/__init__.py +0 -0
  65. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/signals/nlp.py +0 -0
  66. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  67. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  68. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  69. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  70. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  71. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  72. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  73. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  74. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  75. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/state.py +0 -0
  76. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/style/__init__.py +0 -0
  77. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/style/matchers.py +0 -0
  78. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/style/scope.py +0 -0
  79. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/style/types.py +0 -0
  80. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/tasks.py +0 -0
  81. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  82. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/testing/__init__.py +0 -0
  83. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/testing/helpers.py +0 -0
  84. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/testing/session_cache.py +0 -0
  85. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/testing/types.py +0 -0
  86. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/tests/__init__.py +0 -0
  87. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/tests/helpers.py +0 -0
  88. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/types.py +0 -0
  89. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/util/__init__.py +0 -0
  90. {capt_hook-3.4.0 → capt_hook-3.6.0}/captain_hook/util/model_cache.py +0 -0
  91. {capt_hook-3.4.0 → capt_hook-3.6.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.4.0
3
+ Version: 3.6.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
@@ -66,7 +66,7 @@ captain-hook needs no install — it runs through [uvx](https://docs.astral.sh/u
66
66
  uvx capt-hook init
67
67
  ```
68
68
 
69
- `init` scaffolds `.claude/hooks/`, wires Claude Code's settings, installs the bundled skills, and arms the [session reviewer](#it-learns-from-your-corrections). Or install the plugin and let Claude do it. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
69
+ `init` scaffolds `.claude/hooks/`, wires Claude Code's settings, registers the captain-hook plugin so its skills install on workspace-trust, and arms the [session reviewer](#it-learns-from-your-corrections). Or do it all from a session. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
70
70
 
71
71
  ## Your first hook
72
72
 
@@ -19,7 +19,7 @@ captain-hook needs no install — it runs through [uvx](https://docs.astral.sh/u
19
19
  uvx capt-hook init
20
20
  ```
21
21
 
22
- `init` scaffolds `.claude/hooks/`, wires Claude Code's settings, installs the bundled skills, and arms the [session reviewer](#it-learns-from-your-corrections). Or install the plugin and let Claude do it. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
22
+ `init` scaffolds `.claude/hooks/`, wires Claude Code's settings, registers the captain-hook plugin so its skills install on workspace-trust, and arms the [session reviewer](#it-learns-from-your-corrections). Or do it all from a session. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
23
23
 
24
24
  ## Your first hook
25
25
 
@@ -54,40 +54,24 @@ def example_hook_source() -> str:
54
54
  return (importlib.resources.files("captain_hook") / "templates" / "example_hook.py.tmpl").read_text()
55
55
 
56
56
 
57
- def install_skills(root: Path, *, force: bool = False) -> dict[str, str]:
58
- """Copy the bundled Claude Code skills into ``root/.claude/skills``.
57
+ def plugin_dir() -> Path:
58
+ """Filesystem path to the bundled captain-hook plugin root.
59
59
 
60
- Args:
61
- root: Project root receiving the skills.
62
- force: Replace existing skill directories wholesale instead of skipping them.
63
-
64
- Returns:
65
- Per-skill status of ``"installed"``, ``"replaced"``, or ``"skipped"``.
60
+ Holds ``.claude-plugin/plugin.json`` and ``skills/``, so ``claude --plugin-dir``
61
+ can load the skills in-place from the installed wheel without a marketplace clone.
66
62
  """
67
- dest_root = root / ".claude" / "skills"
68
- summary: dict[str, str] = {}
69
- with importlib.resources.as_file(importlib.resources.files("captain_hook") / "skills") as src_root:
70
- for skill in sorted(p for p in src_root.iterdir() if p.is_dir()):
71
- dest = dest_root / skill.name
72
- if dest.exists() and not force:
73
- summary[skill.name] = "skipped"
74
- continue
75
- if dest.exists():
76
- shutil.rmtree(dest)
77
- summary[skill.name] = "replaced"
78
- else:
79
- summary[skill.name] = "installed"
80
- shutil.copytree(skill, dest)
81
- return summary
63
+ return Path(str(importlib.resources.files("captain_hook")))
82
64
 
83
65
 
84
66
  def register_marketplace(root: Path) -> None:
85
- """Enable the captain-hook plugin marketplace in ``root/.claude/settings.local.json``.
67
+ """Enable the captain-hook plugin marketplace in ``root/.claude/settings.json``.
86
68
 
87
69
  Merges ``extraKnownMarketplaces`` and ``enabledPlugins`` entries into the
88
- existing settings so the bundled skills track the repository as a plugin.
70
+ committed settings so the skills load from the plugin (tracking the repository)
71
+ instead of being copied into ``.claude/skills``. Claude Code prompts to install
72
+ the plugin when the project folder is trusted.
89
73
  """
90
- settings_path = root / ".claude" / "settings.local.json"
74
+ settings_path = root / ".claude" / "settings.json"
91
75
  existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
92
76
  write_settings(
93
77
  settings_path,
@@ -104,7 +88,9 @@ def maybe_launch_bootstrap(root: Path) -> bool:
104
88
 
105
89
  Only fires in an interactive session with the ``claude`` CLI on PATH; CI and
106
90
  scripted runs skip the prompt entirely. On acceptance, the captain-hook plugin
107
- marketplace is registered in ``.claude/settings.local.json`` before launching.
91
+ marketplace is registered in ``.claude/settings.json``, and Claude is launched
92
+ with the bundled plugin loaded via ``--plugin-dir`` so the namespaced skill
93
+ resolves immediately without waiting on a marketplace install.
108
94
 
109
95
  Returns:
110
96
  Whether Claude was launched.
@@ -114,7 +100,9 @@ def maybe_launch_bootstrap(root: Path) -> bool:
114
100
  if not click.confirm("Bootstrap hooks now? (launches Claude with the bootstrapping-hooks skill)", default=True):
115
101
  return False
116
102
  register_marketplace(root)
117
- subprocess.run(["claude", "/bootstrapping-hooks"], cwd=root, check=False)
103
+ subprocess.run(
104
+ ["claude", "--plugin-dir", str(plugin_dir()), "/captain-hook:bootstrapping-hooks"], cwd=root, check=False
105
+ )
118
106
  return True
119
107
 
120
108
 
@@ -326,17 +314,14 @@ def init_project(root: Path, *, review: bool = True) -> None:
326
314
  merged, summary = merge_settings(".claude/hooks", settings_path)
327
315
  write_settings(settings_path, merged)
328
316
 
329
- skills_summary = install_skills(root)
317
+ register_marketplace(root)
330
318
 
331
319
  click.echo(f"Scaffolded {example.relative_to(root)} + {settings_path.relative_to(root)}.")
332
320
  click.echo()
333
321
  print_hook_summary(str(settings_path.relative_to(root)), summary)
334
322
  click.echo()
335
- click.echo(".claude/skills/:")
336
- for name in (n for n, status in skills_summary.items() if status == "installed"):
337
- click.echo(f" + installed {name}")
338
- if skipped := [n for n, status in skills_summary.items() if status == "skipped"]:
339
- click.echo(f" unchanged: {', '.join(skipped)} (already present; capt-hook skills install --force to refresh)")
323
+ click.echo("Claude Code plugin:")
324
+ click.echo(f" + registered {PLUGIN_ID} in .claude/settings.json (skills install on folder-trust)")
340
325
  click.echo()
341
326
  match (review, repo_key(root)):
342
327
  case (False, _):
@@ -545,12 +530,11 @@ def skills() -> None:
545
530
 
546
531
 
547
532
  @skills.command(name="install")
548
- @click.option("--force", is_flag=True, default=False, help="Replace skills that already exist in .claude/skills")
549
533
  @click.pass_obj
550
- def skills_install(state: CliState, force: bool) -> None:
551
- """Copy the bundled skills into .claude/skills/."""
552
- for name, status in install_skills(state.root, force=force).items():
553
- click.echo(f" {status} {name}")
534
+ def skills_install(state: CliState) -> None:
535
+ """Register the captain-hook plugin in .claude/settings.json (skills load from the plugin, not copied files)."""
536
+ register_marketplace(state.root)
537
+ click.echo(f" registered {PLUGIN_ID} in .claude/settings.json")
554
538
 
555
539
 
556
540
  @cli.group()
@@ -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 == PACK_MANIFEST or not prefix or m.path == rel or m.path.startswith(prefix):
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
- manifest_member = next((m for m in members if m.path == PACK_MANIFEST), None)
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 / PACK_MANIFEST)
211
- tf.extractall(staging, members=list(members_under(members, manifest.hooks)), filter="data")
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 / PACK_MANIFEST).is_file()}
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 / PACK_MANIFEST)
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 / PACK_MANIFEST)
257
+ manifest = PackManifest.load(manifest_in(cached))
242
258
  return ResolvedPack(entry, manifest.hooks_dir(cached), manifest)
243
259
 
244
260
 
@@ -120,12 +120,12 @@ def spawn(transcript: Path, cwd: str | None) -> None:
120
120
  @review.command()
121
121
  @click.pass_obj
122
122
  def enable(state: CliState) -> None:
123
- """Watch the current repo, install the reviewer's skills, and wire the SessionEnd hook."""
124
- from captain_hook.cli import install_skills
123
+ """Watch the current repo, register the captain-hook plugin, and wire the SessionEnd hook."""
124
+ from captain_hook.cli import register_marketplace
125
125
 
126
126
  repo = current_repo(state.root)
127
127
  watch_repo(repo)
128
- install_skills(state.root)
128
+ register_marketplace(state.root)
129
129
  wired = ensure_review_wiring(state.root / ".claude" / "settings.local.json")
130
130
  click.echo(f"watching {repo}" + (" (SessionEnd hook wired into .claude/settings.local.json)" if wired else ""))
131
131
 
@@ -134,7 +134,7 @@ def brain_prompt(transcript: Path) -> str:
134
134
  from captain_hook.review.scan import REVIEWER_MARKER
135
135
 
136
136
  return (
137
- f"/scanning-sessions --transcript {transcript}\n\n"
137
+ f"/captain-hook:scanning-sessions --transcript {transcript}\n\n"
138
138
  f"[{REVIEWER_MARKER}] Review this repo's eligible candidates and open at most one pull request per"
139
139
  " candidate. Work in one continuous run: do not stop to summarize after drafting — you are done only"
140
140
  " when every eligible candidate has a PR recorded via `review update <id> pr_open --pr-url <url>` or"
@@ -143,13 +143,22 @@ def brain_prompt(transcript: Path) -> str:
143
143
 
144
144
 
145
145
  def brain_argv(*, max_turns: int, max_budget_usd: float) -> list[str]:
146
+ from captain_hook.cli import plugin_dir
146
147
  from captain_hook.llm import ClaudeBackend
147
148
 
148
149
  backend = ClaudeBackend()
149
150
  argv = backend.build_command(backend.models[BRAIN_TIER], None, agent=True)
150
151
  argv[argv.index("--permission-mode") + 1] = "acceptEdits"
151
152
  argv[argv.index("--max-budget-usd") + 1] = str(max_budget_usd)
152
- return [*argv, "--max-turns", str(max_turns), "--allowedTools", ",".join(BRAIN_ALLOWED_TOOLS)]
153
+ return [
154
+ *argv,
155
+ "--plugin-dir",
156
+ str(plugin_dir()),
157
+ "--max-turns",
158
+ str(max_turns),
159
+ "--allowedTools",
160
+ ",".join(BRAIN_ALLOWED_TOOLS),
161
+ ]
153
162
 
154
163
 
155
164
  def spawn_brain(transcript: Path, *, repo_root: Path, settings: ReviewSettings) -> None:
@@ -70,7 +70,7 @@ has the full decision rules and defaults:
70
70
  | A done-criterion to check once at stop ("run tests before stopping") | `gate(only_if=[...], skip_if=[RanCommand(...)])` |
71
71
  | Advice worth surfacing once per session | `nudge` |
72
72
  | A code-content rule needing AST precision | `lint()` |
73
- | A whole style guide | delegate to the `translating-styleguides` skill |
73
+ | A whole style guide | delegate to the `captain-hook:translating-styleguides` skill |
74
74
 
75
75
  Worked, test-passing code for each shape:
76
76
  [pattern catalog](references/pattern-catalog.md).
@@ -364,6 +364,6 @@ CONTRIBUTING/AGENTS/CLAUDE.
364
364
 
365
365
  Found one? Stop. Do not write `StyleRule`s, `lint` approximations of style rules, or a
366
366
  `style.py` here. Offer the category E menu option and, if approved, invoke the
367
- `translating-styleguides` skill with the markdown path — it owns rule atomization, the
367
+ `captain-hook:translating-styleguides` skill with the markdown path — it owns rule atomization, the
368
368
  Matcher/`check()`/LLM tier decision, `style.py`, and its own enforcement report. If the
369
369
  Skill tool is unavailable, read that skill's `SKILL.md` and follow it (both ship together).
@@ -119,7 +119,7 @@ that already had its own `.claude/hooks/`, `init` leaves those files untouched a
119
119
 
120
120
  One file per approved category: `safety.py`, `quality.py`, `testing.py`, `workflow.py`
121
121
  (+ `style.py`, owned end-to-end by `translating-styleguides`). Drafting is delegated:
122
- for each approved hook, invoke the `authoring-hooks` skill via the Skill tool, passing
122
+ for each approved hook, invoke the `captain-hook:authoring-hooks` skill via the Skill tool, passing
123
123
 
124
124
  - the **source quote, verbatim** (it becomes the citation inside the message — the
125
125
  agent being blocked learns *why*),
@@ -225,7 +225,7 @@ Report row:
225
225
 
226
226
  When the survey finds a style guide (`STYLEGUIDE.md`, a "Code style" section in
227
227
  CONTRIBUTING/AGENTS/CLAUDE, `docs/style*.md`), this skill **never** writes `StyleRule`s
228
- itself. If the user approves the category E option, invoke the `translating-styleguides`
228
+ itself. If the user approves the category E option, invoke the `captain-hook:translating-styleguides`
229
229
  skill via the Skill tool with the markdown path as args; it owns `style.py` end-to-end and
230
230
  its enforcement report is appended to this skill's final report. If the Skill tool is
231
231
  unavailable, read that skill's `SKILL.md` directly and follow it — both skills ship together.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: scanning-sessions
3
- description: The headless session-reviewer brain — turns a watched repo's PR-eligible candidates into pull requests, both kinds. Invoked as /scanning-sessions --transcript <path> inside the target repo by capt-hook's detached SessionEnd reviewer pipeline. Enumerates judge-accepted, threshold-eligible candidates via uvx capt-hook review (the CLI is the source of truth), re-verifies every cited correction or complaint verbatim against its session transcript, then per candidate drafts a new hook (create candidates) or amends the attributed misfiring hook with a regression test (fix candidates) in a worktree by delegating to the authoring-hooks skill, proves it with uvx capt-hook test, opens exactly one PR with the verbatim evidence, and records the PR on the candidate. Use when a prompt starts with /scanning-sessions, or to review eligible capt-hook candidates and open hook PRs.
3
+ description: The headless session-reviewer brain — turns a watched repo's PR-eligible candidates into pull requests, both kinds. Invoked as /captain-hook:scanning-sessions --transcript <path> inside the target repo by capt-hook's detached SessionEnd reviewer pipeline. Enumerates judge-accepted, threshold-eligible candidates via uvx capt-hook review (the CLI is the source of truth), re-verifies every cited correction or complaint verbatim against its session transcript, then per candidate drafts a new hook (create candidates) or amends the attributed misfiring hook with a regression test (fix candidates) in a worktree by delegating to the authoring-hooks skill, proves it with uvx capt-hook test, opens exactly one PR with the verbatim evidence, and records the PR on the candidate. Use when a prompt starts with /captain-hook:scanning-sessions, or to review eligible capt-hook candidates and open hook PRs.
4
4
  argument-hint: "--transcript <path to the ended session's transcript>"
5
5
  allowed-tools: Read, Grep, Glob, Bash, Skill
6
6
  ---
@@ -46,7 +46,7 @@ user feedback next pass.
46
46
  the candidate stays `watching`, and you never open a PR against a hook that is no
47
47
  longer there.
48
48
  - **You do not draft hooks yourself** — this skill carries no Write/Edit. Drafting
49
- happens inside the `authoring-hooks` skill, invoked via the Skill tool.
49
+ happens inside the `captain-hook:authoring-hooks` skill, invoked via the Skill tool.
50
50
  - **`uvx capt-hook test` must be green** in the worktree before `gh pr create`.
51
51
  - **Run to completion — never stop early.** You run headless; a text-only reply ends
52
52
  the session immediately. After the authoring-hooks skill returns, keep going in the
@@ -126,7 +126,7 @@ Follow [references/pr-workflow.md](references/pr-workflow.md) exactly:
126
126
 
127
127
  1. **Worktree** — fetch, then `git worktree add` a `capt-hook/review/<rule-slug>`
128
128
  branch off `origin/<default>`.
129
- 2. **Draft** — invoke the `authoring-hooks` skill via the Skill tool, passing the
129
+ 2. **Draft** — invoke the `captain-hook:authoring-hooks` skill via the Skill tool, passing the
130
130
  verbatim correction, its context, and the worktree path. It picks the primitive,
131
131
  writes `.claude/hooks/<slug>.py` with inline tests (one firing on the offending
132
132
  shape, one `Allow()` on a benign neighbor), and runs `uvx capt-hook test`. For a
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
- version = "3.4.0"
3
+ version = "3.6.0"
4
4
  description = "Declarative hook framework for Claude Code"
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
File without changes
File without changes
File without changes