capt-hook 8.1.0__tar.gz → 8.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-8.1.0 → capt_hook-8.3.0}/PKG-INFO +1 -1
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/app.py +4 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/cli.py +78 -14
- capt_hook-8.3.0/captain_hook/packs/general/capt-hook.toml +5 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/models.py +62 -18
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/manager.py +108 -5
- {capt_hook-8.1.0 → capt_hook-8.3.0}/pyproject.toml +1 -1
- capt_hook-8.1.0/captain_hook/packs/general/capt-hook.toml +0 -5
- {capt_hook-8.1.0 → capt_hook-8.3.0}/LICENSE +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/README.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/__main__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/conditions.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/context.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/contexts.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/decisions.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/durable.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/events.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/fields.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/file.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/loader.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/log.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/fixes/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/_lib.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/comments.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/detours.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/general/tombstones.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/packs/steering/steering.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/prompt.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/py.typed +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/store.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/session.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/settings.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/state.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/style/types.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/tasks.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/types.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/util/automode.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/util/http.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/util/proc.py +0 -0
- {capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/util/text.py +0 -0
|
@@ -106,6 +106,9 @@ class State:
|
|
|
106
106
|
settings: HooksSettings | None = None
|
|
107
107
|
classifier: UserClassifier | None = None
|
|
108
108
|
load_errors: list[tuple[str, BaseException]] = field(default_factory=list)
|
|
109
|
+
# Events subscribed only by session-attached packs — excluded from settings-drift
|
|
110
|
+
# warnings, since a plugin wires them in its own hooks.json, not the project's settings.
|
|
111
|
+
attach_only_events: set[str] = field(default_factory=set)
|
|
109
112
|
counter: int = field(default=0, repr=False)
|
|
110
113
|
|
|
111
114
|
|
|
@@ -116,6 +119,7 @@ def reset() -> None:
|
|
|
116
119
|
_state.hooks.clear()
|
|
117
120
|
_state.gitignore_patterns.clear()
|
|
118
121
|
_state.load_errors.clear()
|
|
122
|
+
_state.attach_only_events.clear()
|
|
119
123
|
_state.counter = 0
|
|
120
124
|
_state.settings = None
|
|
121
125
|
_state.classifier = None
|
|
@@ -15,6 +15,7 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
import click
|
|
17
17
|
from cc_transcript.ids import SessionId
|
|
18
|
+
from loguru import logger
|
|
18
19
|
|
|
19
20
|
from captain_hook.app import _state, load_gitignore, reset
|
|
20
21
|
from captain_hook.context import HookContext, load_transcript
|
|
@@ -36,22 +37,62 @@ class CliState:
|
|
|
36
37
|
root: Path
|
|
37
38
|
hooks: str
|
|
38
39
|
|
|
39
|
-
def discover(self) -> list[manager.ResolvedPack]:
|
|
40
|
+
def discover(self, session_dir: Path | None = None) -> list[manager.ResolvedPack]:
|
|
40
41
|
reset()
|
|
41
42
|
load_gitignore(self.root)
|
|
42
43
|
discover_hooks(self.hooks)
|
|
43
44
|
resolved, missing = manager.resolve_enabled_packs(self.root)
|
|
44
45
|
for pack_ in resolved:
|
|
45
46
|
discover_pack(pack_.entry.name, pack_.path)
|
|
46
|
-
|
|
47
|
+
# NLP provisioning is one shared SessionStart hook. Register it before the attached
|
|
48
|
+
# span when a project (packs.toml) pack needs it, so it counts as project-owned; only
|
|
49
|
+
# when solely an attached pack needs it does it register inside the span, so an
|
|
50
|
+
# attach-only NLP hook is attributed to the attach and never flagged as settings drift.
|
|
51
|
+
project_nlp = any(pack_.manifest.nlp for pack_ in resolved)
|
|
52
|
+
if project_nlp:
|
|
47
53
|
register_nlp_provisioning()
|
|
54
|
+
attached = self.attached_packs(resolved, session_dir)
|
|
55
|
+
attached_start = len(_state.hooks)
|
|
56
|
+
for pack_ in attached:
|
|
57
|
+
discover_pack(pack_.entry.name, pack_.path)
|
|
58
|
+
if not project_nlp and any(pack_.manifest.nlp for pack_ in attached):
|
|
59
|
+
register_nlp_provisioning()
|
|
60
|
+
attached_hooks = _state.hooks[attached_start:]
|
|
61
|
+
all_resolved = [*resolved, *attached]
|
|
62
|
+
attached_ids = {id(h) for h in attached_hooks}
|
|
63
|
+
_state.attach_only_events = {n for h in attached_hooks for n in event_names(h.spec.events)} - {
|
|
64
|
+
n for h in _state.hooks if id(h) not in attached_ids for n in event_names(h.spec.events)
|
|
65
|
+
}
|
|
48
66
|
if missing:
|
|
49
67
|
print(
|
|
50
68
|
f"capt-hook: packs unavailable (offline and not cached): {', '.join(missing)} "
|
|
51
69
|
"— run `capt-hook pack update` when online",
|
|
52
70
|
file=sys.stderr,
|
|
53
71
|
)
|
|
54
|
-
return
|
|
72
|
+
return all_resolved
|
|
73
|
+
|
|
74
|
+
def attached_packs(
|
|
75
|
+
self, resolved: Sequence[manager.ResolvedPack], session_dir: Path | None
|
|
76
|
+
) -> list[manager.ResolvedPack]:
|
|
77
|
+
"""Resolve session-attached packs a builtin or packs.toml pack doesn't already own.
|
|
78
|
+
|
|
79
|
+
A packs.toml entry (builtin, source, or ``disabled = true``) of the same name wins
|
|
80
|
+
over an ambient plugin attach; the shadowed attach is dropped with a debug log.
|
|
81
|
+
"""
|
|
82
|
+
if session_dir is None:
|
|
83
|
+
return []
|
|
84
|
+
owned = {pack_.entry.name for pack_ in resolved} | {
|
|
85
|
+
e.name
|
|
86
|
+
for e in manager.read_entries(manager.packs_toml_path(self.root))
|
|
87
|
+
if isinstance(e, manager.DisabledPack)
|
|
88
|
+
}
|
|
89
|
+
kept: list[manager.ResolvedPack] = []
|
|
90
|
+
for pack_ in manager.resolve_attached(session_dir):
|
|
91
|
+
if pack_.entry.name in owned:
|
|
92
|
+
logger.bind(pack=pack_.entry.name).debug("attached pack shadowed by a packs.toml or disabled entry")
|
|
93
|
+
continue
|
|
94
|
+
kept.append(pack_)
|
|
95
|
+
return kept
|
|
55
96
|
|
|
56
97
|
|
|
57
98
|
def example_hook_source() -> str:
|
|
@@ -253,7 +294,10 @@ def regenerate_settings(state: CliState) -> None:
|
|
|
253
294
|
|
|
254
295
|
def settings_drift(root: Path) -> set[str]:
|
|
255
296
|
paths = [p for name in ("settings.json", "settings.local.json") if (p := root / ".claude" / name).exists()]
|
|
256
|
-
|
|
297
|
+
if not paths:
|
|
298
|
+
return set()
|
|
299
|
+
wired = {event for p in paths for event in capt_hook_events(p)}
|
|
300
|
+
return subscribed_events() - _state.attach_only_events - wired
|
|
257
301
|
|
|
258
302
|
|
|
259
303
|
def warn_settings_drift(
|
|
@@ -285,12 +329,7 @@ def warn_settings_drift(
|
|
|
285
329
|
return base
|
|
286
330
|
|
|
287
331
|
|
|
288
|
-
def run_event(
|
|
289
|
-
event_name: str,
|
|
290
|
-
*,
|
|
291
|
-
async_: bool = False,
|
|
292
|
-
root: Path | None = None,
|
|
293
|
-
) -> None:
|
|
332
|
+
def run_event(state: CliState, event_name: str, *, async_: bool = False) -> None:
|
|
294
333
|
try:
|
|
295
334
|
event = Event[event_name]
|
|
296
335
|
except KeyError:
|
|
@@ -315,19 +354,22 @@ def run_event(
|
|
|
315
354
|
setup_logging(session_id)
|
|
316
355
|
resolved_path = raw.get("agent_transcript_path") or raw.get("transcript_path")
|
|
317
356
|
|
|
357
|
+
# stdin is parsed first so the session dir is known before discovery: CliState.discover
|
|
358
|
+
# loads this session's attached packs (see attached_packs) on top of packs.toml.
|
|
318
359
|
session_dir = ensure_session(SessionId(session_id)) if session_id else None
|
|
360
|
+
state.discover(session_dir=session_dir)
|
|
319
361
|
ctx = HookContext(
|
|
320
362
|
session=SessionStore(session_dir),
|
|
321
363
|
transcript=load_transcript(resolved_path),
|
|
322
364
|
settings=_state.settings,
|
|
323
|
-
project_root=root,
|
|
365
|
+
project_root=state.root,
|
|
324
366
|
)
|
|
325
367
|
evt = event.event_class(_raw=raw, ctx=ctx)
|
|
326
368
|
|
|
327
369
|
if output := warn_settings_drift(
|
|
328
370
|
dispatch(event, evt, session_dir=session_dir, async_=async_),
|
|
329
371
|
event,
|
|
330
|
-
root,
|
|
372
|
+
state.root,
|
|
331
373
|
session_dir,
|
|
332
374
|
async_=async_,
|
|
333
375
|
):
|
|
@@ -520,8 +562,7 @@ def cli(ctx: click.Context, hooks: str | None, root_path: str | None) -> None:
|
|
|
520
562
|
@click.option("--async", "async_", is_flag=True, default=False, help="Run async hooks only")
|
|
521
563
|
@click.pass_obj
|
|
522
564
|
def run(state: CliState, event: str, async_: bool) -> None:
|
|
523
|
-
state
|
|
524
|
-
run_event(event, async_=async_, root=state.root)
|
|
565
|
+
run_event(state, event, async_=async_)
|
|
525
566
|
|
|
526
567
|
|
|
527
568
|
@cli.command(name="register-hooks")
|
|
@@ -622,6 +663,29 @@ def pack_add(state: CliState, target: str) -> None:
|
|
|
622
663
|
regenerate_settings(state)
|
|
623
664
|
|
|
624
665
|
|
|
666
|
+
@pack.command(name="attach")
|
|
667
|
+
@click.argument("directory")
|
|
668
|
+
def pack_attach(directory: str) -> None:
|
|
669
|
+
"""Register a plugin's pack for the current session (reads the SessionStart JSON on stdin).
|
|
670
|
+
|
|
671
|
+
A Claude plugin wires this to a SessionStart hook so its pack loads under the byte-identical
|
|
672
|
+
canonical ``uvx capt-hook run <Event>`` commands, letting Claude Code's exact-command dedup
|
|
673
|
+
collapse plugin and project wiring into one process per event. Writes nothing to stdout on
|
|
674
|
+
success (SessionStart stdout is injected into the model context); a missing or invalid
|
|
675
|
+
manifest exits 1 with a stderr message.
|
|
676
|
+
"""
|
|
677
|
+
raw = json.loads(sys.stdin.read())
|
|
678
|
+
session_dir = ensure_session(SessionId(raw["session_id"]))
|
|
679
|
+
root = Path(directory).resolve()
|
|
680
|
+
try:
|
|
681
|
+
manifest = manager.PackManifest.load(manager.manifest_in(root))
|
|
682
|
+
except manager.PackError as e:
|
|
683
|
+
raise click.ClickException(str(e)) from e
|
|
684
|
+
manager.upsert_attached(
|
|
685
|
+
session_dir, manager.AttachedPack(name=manifest.name, dir=str(root), version=manifest.version)
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
625
689
|
@pack.command(name="list")
|
|
626
690
|
@click.pass_obj
|
|
627
691
|
def pack_list(state: CliState) -> None:
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
name = "general"
|
|
2
|
+
version = "0.13.0"
|
|
3
|
+
description = "Language-agnostic guards: git/command safety, doc & prompt nudges, task & plan discipline, model-routing nudges (review, security audit, and diagnosis route to gpt-5.5), code-review + docs-freshness gates, a detour check-in nudge, a verbose-comment nudge, and delegated-prose nudges that catch doc writing handed to a subagent or workflow without the writing-docs skill."
|
|
4
|
+
hooks = "."
|
|
5
|
+
nlp = true
|
|
@@ -321,7 +321,8 @@ The Models rubric: implementation delegates to opus-4.8 at xhigh — opus is ~2x
|
|
|
321
321
|
than fable and nearly as capable. Fable's lanes are orchestration, design/architecture
|
|
322
322
|
review, hard planning, all prose/writing, and implementation that is very sensitive or
|
|
323
323
|
error-prone (auth, migrations, concurrency, data loss, crypto, subtle algorithms).
|
|
324
|
-
Code/diff review and bug diagnosis have their own gpt-5.5
|
|
324
|
+
Code/diff review, security review/audit, and bug diagnosis have their own gpt-5.5
|
|
325
|
+
lanes with separate nudges.
|
|
325
326
|
|
|
326
327
|
Set fire=true only when the prompt is clearly routine implementation — building, fixing,
|
|
327
328
|
wiring, or refactoring code — with no fable-lane signal. A prompt that reviews, plans,
|
|
@@ -476,20 +477,25 @@ Auth plus concurrency is a sensitive surface — fable's inline lane.
|
|
|
476
477
|
)
|
|
477
478
|
|
|
478
479
|
llm_nudge(
|
|
479
|
-
"""Decide whether this delegated subagent runs code review
|
|
480
|
-
|
|
480
|
+
"""Decide whether this delegated subagent runs code review, a security review/audit
|
|
481
|
+
or verification of security-sensitive code, or bug diagnosis that should route to
|
|
482
|
+
gpt-5.5 instead of fable.
|
|
481
483
|
|
|
482
484
|
<delegated_spawn> holds the pending Agent/Task call: its model pin (or that it inherits
|
|
483
485
|
the session model, fable), agent type, and prompt.
|
|
484
486
|
|
|
485
487
|
The Models rubric: code/diff review — sweeping a diff or codebase for bugs,
|
|
486
|
-
correctness, or cleanups; finder and refuter passes over findings —
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
488
|
+
correctness, or cleanups; finder and refuter passes over findings — security
|
|
489
|
+
review/audit and verification of security-sensitive code (auth, input validation,
|
|
490
|
+
crypto, secrets), and bug diagnosis route to gpt-5.5 via the codex skill; fable is
|
|
491
|
+
the escalation target when gpt-5.5's output misses. Fable keeps design/architecture
|
|
492
|
+
review, "is this the right approach" judgment, prose review, the synthesis/
|
|
493
|
+
accept-reject pass over review findings — and security-sensitive implementation,
|
|
494
|
+
which is not review.
|
|
495
|
+
|
|
496
|
+
Set fire=true only when the prompt clearly reviews code or diffs for defects, audits
|
|
497
|
+
or verifies security-sensitive code, or diagnoses a bug, and the spawn would run on
|
|
498
|
+
fable. Design review, approach judgment,
|
|
493
499
|
synthesis over findings, and prose review are fable's lanes: fire=false. When
|
|
494
500
|
uncertain, fire=false — the agent may have chosen fable deliberately, and a false
|
|
495
501
|
alarm teaches it to ignore this nudge. Keep reasoning under 40 words.
|
|
@@ -519,10 +525,23 @@ The accept-reject pass over findings stays on fable.
|
|
|
519
525
|
Review the README draft for factual errors.
|
|
520
526
|
Prose review stays on fable.
|
|
521
527
|
</example>
|
|
528
|
+
<example fire="true">
|
|
529
|
+
Audit the session-token handling in auth/middleware.py for vulnerabilities.
|
|
530
|
+
Security review/audit of code — gpt-5.5's lane via codex.
|
|
531
|
+
</example>
|
|
532
|
+
<example fire="true">
|
|
533
|
+
Verify the new input-validation layer rejects path traversal and injection payloads.
|
|
534
|
+
Verification of security-sensitive code routes to gpt-5.5.
|
|
535
|
+
</example>
|
|
536
|
+
<example fire="false">
|
|
537
|
+
Implement mitigations for the security-audit findings in auth.py.
|
|
538
|
+
Security-sensitive implementation, not review — the implementation lanes apply.
|
|
539
|
+
</example>
|
|
522
540
|
</examples>""",
|
|
523
541
|
message=lambda r: (
|
|
524
542
|
f"This review/diagnosis delegation would run on fable. {r.reasoning} "
|
|
525
|
-
"Code/diff review
|
|
543
|
+
"Code/diff review, security review/audit and verification of security-sensitive code, "
|
|
544
|
+
"and bug diagnosis route to gpt-5.5: run the codex skill (from a "
|
|
526
545
|
"workflow or subagent, spawn a model='sonnet', effort='low' wrapper that writes a "
|
|
527
546
|
"self-contained codex prompt), and escalate to fable only when gpt-5.5's output misses. "
|
|
528
547
|
"Design review and findings synthesis stay on fable. "
|
|
@@ -532,7 +551,11 @@ Prose review stays on fable.
|
|
|
532
551
|
events=Event.PreToolUse,
|
|
533
552
|
only_if=[
|
|
534
553
|
Tool("Agent|Task"),
|
|
535
|
-
ToolInput(
|
|
554
|
+
ToolInput(
|
|
555
|
+
"prompt",
|
|
556
|
+
r"(?i)(\b(review|refut|adversari|audit|correctness|diagnos|root.?caus|secur|vuln|pentest)"
|
|
557
|
+
r"|\bverif\w*[\s\S]{0,160}?\b(auth|crypt|secret|sanitiz|inject|input.?valid|token|session))",
|
|
558
|
+
),
|
|
536
559
|
],
|
|
537
560
|
skip_if=[
|
|
538
561
|
ToolInput("model", r"(?i)\b(opus|sonnet|haiku)\b"),
|
|
@@ -552,6 +575,13 @@ Prose review stays on fable.
|
|
|
552
575
|
prompt="Synthesize the confirmed review findings and decide which to fix",
|
|
553
576
|
llm={"fire": False},
|
|
554
577
|
): Allow(),
|
|
578
|
+
Input(prompt="Audit auth/session.py for security vulnerabilities"): Warn(pattern="gpt-5.5"),
|
|
579
|
+
Input(prompt="Verify the input-validation change blocks path traversal"): Warn(pattern="codex"),
|
|
580
|
+
Input(prompt="Verify the pagination change renders the last page correctly"): Allow(),
|
|
581
|
+
Input(
|
|
582
|
+
prompt="Implement mitigations for the security audit findings in auth.py",
|
|
583
|
+
llm={"fire": False},
|
|
584
|
+
): Allow(),
|
|
555
585
|
},
|
|
556
586
|
)
|
|
557
587
|
|
|
@@ -666,10 +696,13 @@ line that pins a model — a stage not quoted there carries no pin and inherits
|
|
|
666
696
|
session model, fable.
|
|
667
697
|
|
|
668
698
|
The Models rubric: code/diff review stages — finder sweeps over a diff or codebase,
|
|
669
|
-
adversarial refuters over findings —
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
699
|
+
adversarial refuters over findings — security review/audit stages and verification
|
|
700
|
+
of security-sensitive code (auth, input validation, crypto, secrets), and bug
|
|
701
|
+
diagnosis route to gpt-5.5 via the codex skill. A stage does that correctly when it
|
|
702
|
+
pins model 'sonnet' at low effort and its prompt writes a self-contained codex prompt
|
|
703
|
+
and runs the codex skill. Fable keeps the synthesis/accept-reject stage over findings
|
|
704
|
+
and design/architecture judgment — and security-sensitive implementation, which is
|
|
705
|
+
not review.
|
|
673
706
|
|
|
674
707
|
Set fire=true only when at least one review or diagnosis stage would run on fable —
|
|
675
708
|
unpinned, or pinned 'fable'. Stages already wrapped for codex, synthesis stages, and
|
|
@@ -695,10 +728,14 @@ Already the codex wrapper — correctly routed.
|
|
|
695
728
|
agent(`Synthesize the confirmed findings and decide which to fix`)
|
|
696
729
|
Synthesis/accept-reject stays on fable.
|
|
697
730
|
</example>
|
|
731
|
+
<example fire="true">
|
|
732
|
+
agent(`Audit the auth flow for injection and session-fixation issues; return findings as JSON`)
|
|
733
|
+
An unpinned security audit inherits fable; security review/audit is the codex-wrapper lane.
|
|
734
|
+
</example>
|
|
698
735
|
</examples>""",
|
|
699
736
|
message=lambda r: (
|
|
700
737
|
f"This workflow runs review/diagnosis stages on fable. {r.reasoning} "
|
|
701
|
-
"Route finder, refuter, and diagnosis stages to gpt-5.5: make each a model: 'sonnet', "
|
|
738
|
+
"Route finder, refuter, security-audit, and diagnosis stages to gpt-5.5: make each a model: 'sonnet', "
|
|
702
739
|
"effort: 'low' stage that writes a self-contained codex prompt and runs the codex skill; "
|
|
703
740
|
"keep the synthesis/accept-reject stage on fable (inherit the session model). "
|
|
704
741
|
"See CLAUDE.md § Plan Execution & Orchestration (Models)."
|
|
@@ -707,7 +744,10 @@ Synthesis/accept-reject stays on fable.
|
|
|
707
744
|
events=Event.PreToolUse,
|
|
708
745
|
only_if=[
|
|
709
746
|
Tool("Workflow"),
|
|
710
|
-
WorkflowScript(
|
|
747
|
+
WorkflowScript(
|
|
748
|
+
pattern=r"(?i)(\b(review|refut|adversari|audit|correctness|diagnos|root.?caus|secur|vuln|pentest)"
|
|
749
|
+
r"|\bverif\w*[\s\S]{0,160}?\b(auth|crypt|secret|sanitiz|inject|input.?valid|token|session))",
|
|
750
|
+
),
|
|
711
751
|
],
|
|
712
752
|
max_fires=2,
|
|
713
753
|
max_context=16_000,
|
|
@@ -730,6 +770,10 @@ Synthesis/accept-reject stays on fable.
|
|
|
730
770
|
script="agent(`Synthesize the confirmed review findings and decide which to fix`)",
|
|
731
771
|
llm={"fire": False},
|
|
732
772
|
): Allow(),
|
|
773
|
+
Input(
|
|
774
|
+
script="agent(`Audit the login flow for auth bypass and injection; return findings as JSON`)"
|
|
775
|
+
): Warn(pattern="gpt-5.5"),
|
|
776
|
+
Input(script="agent('Verify the CLI renders the last page correctly')"): Allow(),
|
|
733
777
|
},
|
|
734
778
|
)
|
|
735
779
|
|
|
@@ -22,6 +22,7 @@ import os
|
|
|
22
22
|
import re
|
|
23
23
|
import shutil
|
|
24
24
|
import tarfile
|
|
25
|
+
import tempfile
|
|
25
26
|
import time
|
|
26
27
|
import tomllib
|
|
27
28
|
from collections.abc import Iterator, Sequence
|
|
@@ -31,11 +32,13 @@ from pathlib import Path
|
|
|
31
32
|
from typing import Any
|
|
32
33
|
|
|
33
34
|
from filelock import FileLock
|
|
35
|
+
from loguru import logger
|
|
34
36
|
|
|
35
37
|
from captain_hook.settings import resolve_cache_dir
|
|
36
38
|
from captain_hook.util import http
|
|
37
39
|
|
|
38
40
|
PACK_MANIFEST = "capt-hook.toml"
|
|
41
|
+
ATTACHED_FILE = "attached_packs.json"
|
|
39
42
|
SHA_MARKER = ".sha"
|
|
40
43
|
LATEST_REF = "latest"
|
|
41
44
|
# Moving refs (@latest / a branch / a bare default-branch source) re-resolve to a fresh
|
|
@@ -108,12 +111,39 @@ class ExternalPack:
|
|
|
108
111
|
commit: str | None = None
|
|
109
112
|
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class DisabledPack:
|
|
116
|
+
"""A packs.toml entry that declines a pack by name.
|
|
117
|
+
|
|
118
|
+
``[packs.<name>] disabled = true`` suppresses a pack the repo would otherwise
|
|
119
|
+
inherit — a builtin, a packs.toml source, or a plugin-attached pack — regardless of
|
|
120
|
+
any other keys on the entry. Disabling always wins.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
name: str
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True, slots=True)
|
|
127
|
+
class AttachedPack:
|
|
128
|
+
"""A pack a Claude plugin registered for the current session via ``pack attach``.
|
|
129
|
+
|
|
130
|
+
Stored per session in ``attached_packs.json`` keyed by ``name``; ``dir`` is the
|
|
131
|
+
absolute pack root (holding the ``capt-hook.toml`` manifest) and ``version`` is the
|
|
132
|
+
manifest version recorded at attach time.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
dir: str
|
|
137
|
+
version: str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
type PackEntry = BuiltinPack | ExternalPack | DisabledPack
|
|
141
|
+
type ResolvedEntry = BuiltinPack | ExternalPack | AttachedPack
|
|
112
142
|
|
|
113
143
|
|
|
114
144
|
@dataclass(frozen=True, slots=True)
|
|
115
145
|
class ResolvedPack:
|
|
116
|
-
entry:
|
|
146
|
+
entry: ResolvedEntry
|
|
117
147
|
path: Path
|
|
118
148
|
manifest: PackManifest
|
|
119
149
|
|
|
@@ -160,6 +190,8 @@ def manifest_in(root: Path) -> Path:
|
|
|
160
190
|
|
|
161
191
|
def parse_entry(name: str, table: dict[str, Any]) -> PackEntry:
|
|
162
192
|
match table:
|
|
193
|
+
case {"disabled": True}:
|
|
194
|
+
return DisabledPack(name=name)
|
|
163
195
|
case {"source": source, "commit": commit}:
|
|
164
196
|
return ExternalPack(name=name, source=PackSource.parse(source), commit=commit)
|
|
165
197
|
case {"source": source}:
|
|
@@ -180,6 +212,8 @@ def render_entry(entry: PackEntry) -> str:
|
|
|
180
212
|
match entry:
|
|
181
213
|
case BuiltinPack(name=name):
|
|
182
214
|
return f"[packs.{name}]\n\n"
|
|
215
|
+
case DisabledPack(name=name):
|
|
216
|
+
return f"[packs.{name}]\ndisabled = true\n\n"
|
|
183
217
|
case ExternalPack(name=name, source=source, commit=None):
|
|
184
218
|
return f'[packs.{name}]\nsource = "{source}"\n\n'
|
|
185
219
|
case ExternalPack(name=name, source=source, commit=commit):
|
|
@@ -191,10 +225,20 @@ def render_packs_toml(entries: Sequence[PackEntry]) -> str:
|
|
|
191
225
|
|
|
192
226
|
|
|
193
227
|
def atomic_write(path: Path, text: str) -> None:
|
|
228
|
+
"""Write ``text`` to ``path`` atomically via a unique temp file in the same directory.
|
|
229
|
+
|
|
230
|
+
The temp name is per-call (``mkstemp``), so concurrent writers to the same path never
|
|
231
|
+
consume each other's temp file — one writer's ``os.replace`` can't yank another's out.
|
|
232
|
+
"""
|
|
194
233
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
195
|
-
tmp = path.
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=f"{path.suffix}.tmp")
|
|
235
|
+
try:
|
|
236
|
+
with os.fdopen(fd, "w") as f:
|
|
237
|
+
f.write(text)
|
|
238
|
+
os.replace(tmp, path)
|
|
239
|
+
except BaseException:
|
|
240
|
+
Path(tmp).unlink(missing_ok=True)
|
|
241
|
+
raise
|
|
198
242
|
|
|
199
243
|
|
|
200
244
|
def upsert_entry(path: Path, entry: PackEntry) -> None:
|
|
@@ -444,6 +488,8 @@ def resolve_enabled_packs(root: Path) -> tuple[list[ResolvedPack], list[str]]:
|
|
|
444
488
|
match entry:
|
|
445
489
|
case BuiltinPack(name=name):
|
|
446
490
|
resolved.append(resolve_builtin(name))
|
|
491
|
+
case DisabledPack():
|
|
492
|
+
pass
|
|
447
493
|
case ExternalPack() as ext if found := (load_cached_fresh(ext, now) if fast else resolve_external(ext)):
|
|
448
494
|
resolved.append(found)
|
|
449
495
|
case ExternalPack() as ext:
|
|
@@ -451,3 +497,60 @@ def resolve_enabled_packs(root: Path) -> tuple[list[ResolvedPack], list[str]]:
|
|
|
451
497
|
if not (fast or missing):
|
|
452
498
|
atomic_write(fastpath_path(root), toml_hash(root))
|
|
453
499
|
return resolved, missing
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def attached_path(session_dir: Path) -> Path:
|
|
503
|
+
return session_dir / ATTACHED_FILE
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def read_attached(session_dir: Path) -> list[AttachedPack]:
|
|
507
|
+
path = attached_path(session_dir)
|
|
508
|
+
if not path.exists():
|
|
509
|
+
return []
|
|
510
|
+
return [AttachedPack(name=e["name"], dir=e["dir"], version=e["version"]) for e in json.loads(path.read_text())]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def upsert_attached(session_dir: Path, pack: AttachedPack) -> None:
|
|
514
|
+
"""Record ``pack`` in the session's attach file, replacing any entry of the same name.
|
|
515
|
+
|
|
516
|
+
The read-modify-write runs under a file lock so two ``pack attach`` processes (parallel
|
|
517
|
+
SessionStart hooks) serialize rather than clobber each other's entries.
|
|
518
|
+
"""
|
|
519
|
+
path = attached_path(session_dir)
|
|
520
|
+
lock = path.with_name(path.name + ".lock")
|
|
521
|
+
with FileLock(str(lock)):
|
|
522
|
+
existing = read_attached(session_dir)
|
|
523
|
+
if (prior := next((p for p in existing if p.name == pack.name), None)) and prior.dir != pack.dir:
|
|
524
|
+
logger.bind(pack=pack.name, previous=prior.dir, incoming=pack.dir).debug(
|
|
525
|
+
"attached pack name re-bound to a different dir; the newer attach wins"
|
|
526
|
+
)
|
|
527
|
+
entries = [*(p for p in existing if p.name != pack.name), pack]
|
|
528
|
+
atomic_write(
|
|
529
|
+
path,
|
|
530
|
+
json.dumps([{"name": p.name, "dir": p.dir, "version": p.version} for p in entries]),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def resolve_attached(session_dir: Path) -> list[ResolvedPack]:
|
|
535
|
+
"""Resolve this session's attached packs, dropping entries that no longer resolve.
|
|
536
|
+
|
|
537
|
+
A plugin update moves or non-atomically rewrites its versioned cache path, so a prior
|
|
538
|
+
session's attach entry can dangle or point at a half-written manifest. SessionStart
|
|
539
|
+
re-attaches every session, so an entry whose dir has vanished or whose manifest is
|
|
540
|
+
missing/malformed is skipped with a debug log rather than killing dispatch for every
|
|
541
|
+
other hook in the event — the same fail-soft shape as ``resolve_enabled_packs``.
|
|
542
|
+
"""
|
|
543
|
+
resolved: list[ResolvedPack] = []
|
|
544
|
+
for pack in read_attached(session_dir):
|
|
545
|
+
root = Path(pack.dir)
|
|
546
|
+
if not root.is_dir():
|
|
547
|
+
continue
|
|
548
|
+
try:
|
|
549
|
+
manifest = PackManifest.load(manifest_in(root))
|
|
550
|
+
except (PackError, tomllib.TOMLDecodeError, KeyError, OSError):
|
|
551
|
+
logger.bind(pack=pack.name, dir=pack.dir).opt(exception=True).debug(
|
|
552
|
+
"skipped attached pack with a missing or malformed manifest"
|
|
553
|
+
)
|
|
554
|
+
continue
|
|
555
|
+
resolved.append(ResolvedPack(pack, manifest.hooks_dir(root), manifest))
|
|
556
|
+
return resolved
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "capt-hook"
|
|
3
3
|
# Inert sentinel — the real version is stamped from the release tag (uv version --frozen).
|
|
4
|
-
version = "8.
|
|
4
|
+
version = "8.3.0"
|
|
5
5
|
description = "Stop repeating yourself to Claude."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
name = "general"
|
|
2
|
-
version = "0.12.0"
|
|
3
|
-
description = "Language-agnostic guards: git/command safety, doc & prompt nudges, task & plan discipline, code-review + docs-freshness gates, a detour check-in nudge, a verbose-comment nudge, and delegated-prose nudges that catch doc writing handed to a subagent or workflow without the writing-docs skill."
|
|
4
|
-
hooks = "."
|
|
5
|
-
nlp = true
|
|
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
|
|
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-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md
RENAMED
|
File without changes
|
{capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-8.1.0 → capt_hook-8.3.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md
RENAMED
|
File without changes
|
{capt_hook-8.1.0 → capt_hook-8.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|