capt-hook 3.5.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.5.0 → capt_hook-3.6.0}/PKG-INFO +1 -1
  2. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/manager.py +25 -9
  3. {capt_hook-3.5.0 → capt_hook-3.6.0}/pyproject.toml +1 -1
  4. {capt_hook-3.5.0 → capt_hook-3.6.0}/LICENSE +0 -0
  5. {capt_hook-3.5.0 → capt_hook-3.6.0}/README.md +0 -0
  6. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  7. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/__init__.py +0 -0
  8. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/__main__.py +0 -0
  9. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/app.py +0 -0
  10. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/classifiers/__init__.py +0 -0
  11. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/classifiers/conductor.py +0 -0
  12. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/classifiers/droid.py +0 -0
  13. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/classifiers/native.py +0 -0
  14. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/cli.py +0 -0
  15. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/command.py +0 -0
  16. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/conditions.py +0 -0
  17. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/context.py +0 -0
  18. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/decisions.py +0 -0
  19. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/dispatch.py +0 -0
  20. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/events.py +0 -0
  21. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/file.py +0 -0
  22. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/llm/__init__.py +0 -0
  23. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/loader.py +0 -0
  24. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/log.py +0 -0
  25. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/__init__.py +0 -0
  26. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/capt-hook.toml +0 -0
  27. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/commands.py +0 -0
  28. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/docs.py +0 -0
  29. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/plans.py +0 -0
  30. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/prompts.py +0 -0
  31. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/review.py +0 -0
  32. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/stewardship.py +0 -0
  33. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/general/tasks.py +0 -0
  34. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  35. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/python/style.py +0 -0
  36. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/python/testing.py +0 -0
  37. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/packs/python/toolchain.py +0 -0
  38. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/primitives/__init__.py +0 -0
  39. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/primitives/commands.py +0 -0
  40. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/primitives/lint.py +0 -0
  41. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/primitives/llm.py +0 -0
  42. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/primitives/nudge.py +0 -0
  43. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/primitives/workflow.py +0 -0
  44. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/prompt.py +0 -0
  45. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/py.typed +0 -0
  46. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/__init__.py +0 -0
  47. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/cli.py +0 -0
  48. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/dashboard.py +0 -0
  49. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/fix.py +0 -0
  50. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/formats.py +0 -0
  51. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/judge.py +0 -0
  52. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/pipeline.py +0 -0
  53. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/repo.py +0 -0
  54. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/scan.py +0 -0
  55. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/settings.py +0 -0
  56. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/store.py +0 -0
  57. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/review/sync.py +0 -0
  58. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/session.py +0 -0
  59. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/settings.py +0 -0
  60. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/signals/__init__.py +0 -0
  61. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/signals/nlp.py +0 -0
  62. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  63. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  64. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  65. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  66. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  67. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  68. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  69. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  70. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  71. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  72. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  73. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  74. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  75. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/state.py +0 -0
  76. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/style/__init__.py +0 -0
  77. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/style/matchers.py +0 -0
  78. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/style/scope.py +0 -0
  79. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/style/types.py +0 -0
  80. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/tasks.py +0 -0
  81. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  82. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/testing/__init__.py +0 -0
  83. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/testing/helpers.py +0 -0
  84. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/testing/session_cache.py +0 -0
  85. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/testing/types.py +0 -0
  86. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/tests/__init__.py +0 -0
  87. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/tests/helpers.py +0 -0
  88. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/types.py +0 -0
  89. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/util/__init__.py +0 -0
  90. {capt_hook-3.5.0 → capt_hook-3.6.0}/captain_hook/util/model_cache.py +0 -0
  91. {capt_hook-3.5.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.5.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
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
- version = "3.5.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
File without changes
File without changes