@@ -19,27 +19,42 @@ class HooksSyncError(Exception):
1919class HookEntry :
2020 """Represents a single hook entry for a lifecycle event."""
2121
22- script : str # Script filename
2322 job_name : str # Job that provides this hook
2423 job_dir : Path # Full path to job directory
24+ script : str | None = None # Script filename (if script-based hook)
25+ module : str | None = None # Python module (if module-based hook)
2526
26- def get_script_path (self , project_path : Path ) -> str :
27+ def get_command (self , project_path : Path ) -> str :
2728 """
28- Get the script path relative to project root .
29+ Get the command to run this hook .
2930
3031 Args:
3132 project_path: Path to project root
3233
3334 Returns:
34- Relative path to script from project root
35+ Command string to execute
3536 """
36- # Script path is: .deepwork/jobs/{job_name}/hooks/{script}
37- script_path = self .job_dir / "hooks" / self .script
38- try :
39- return str (script_path .relative_to (project_path ))
40- except ValueError :
41- # If not relative, return the full path
42- return str (script_path )
37+ if self .module :
38+ # Python module - run directly with python -m
39+ return f"python -m { self .module } "
40+ elif self .script :
41+ # Script path is: .deepwork/jobs/{job_name}/hooks/{script}
42+ script_path = self .job_dir / "hooks" / self .script
43+ try :
44+ return str (script_path .relative_to (project_path ))
45+ except ValueError :
46+ # If not relative, return the full path
47+ return str (script_path )
48+ else :
49+ raise ValueError ("HookEntry must have either script or module" )
50+
51+
52+ @dataclass
53+ class HookSpec :
54+ """Specification for a single hook (either script or module)."""
55+
56+ script : str | None = None
57+ module : str | None = None
4358
4459
4560@dataclass
@@ -48,7 +63,7 @@ class JobHooks:
4863
4964 job_name : str
5065 job_dir : Path
51- hooks : dict [str , list [str ]] = field (default_factory = dict ) # event -> [scripts ]
66+ hooks : dict [str , list [HookSpec ]] = field (default_factory = dict ) # event -> [HookSpec ]
5267
5368 @classmethod
5469 def from_job_dir (cls , job_dir : Path ) -> "JobHooks | None" :
@@ -74,13 +89,23 @@ def from_job_dir(cls, job_dir: Path) -> "JobHooks | None":
7489 if not data or not isinstance (data , dict ):
7590 return None
7691
77- # Parse hooks - each key is an event, value is list of scripts
78- hooks : dict [str , list [str ]] = {}
79- for event , scripts in data .items ():
80- if isinstance (scripts , list ):
81- hooks [event ] = [str (s ) for s in scripts ]
82- elif isinstance (scripts , str ):
83- hooks [event ] = [scripts ]
92+ # Parse hooks - each key is an event, value is list of scripts or module specs
93+ hooks : dict [str , list [HookSpec ]] = {}
94+ for event , entries in data .items ():
95+ if not isinstance (entries , list ):
96+ entries = [entries ]
97+
98+ hook_specs : list [HookSpec ] = []
99+ for entry in entries :
100+ if isinstance (entry , str ):
101+ # Simple script filename
102+ hook_specs .append (HookSpec (script = entry ))
103+ elif isinstance (entry , dict ) and "module" in entry :
104+ # Python module specification
105+ hook_specs .append (HookSpec (module = entry ["module" ]))
106+
107+ if hook_specs :
108+ hooks [event ] = hook_specs
84109
85110 if not hooks :
86111 return None
@@ -134,31 +159,32 @@ def merge_hooks_for_platform(
134159 merged : dict [str , list [dict [str , Any ]]] = {}
135160
136161 for job_hooks in job_hooks_list :
137- for event , scripts in job_hooks .hooks .items ():
162+ for event , hook_specs in job_hooks .hooks .items ():
138163 if event not in merged :
139164 merged [event ] = []
140165
141- for script in scripts :
166+ for spec in hook_specs :
142167 entry = HookEntry (
143- script = script ,
144168 job_name = job_hooks .job_name ,
145169 job_dir = job_hooks .job_dir ,
170+ script = spec .script ,
171+ module = spec .module ,
146172 )
147- script_path = entry .get_script_path (project_path )
173+ command = entry .get_command (project_path )
148174
149175 # Create hook configuration for Claude Code format
150176 hook_config = {
151177 "matcher" : "" , # Match all
152178 "hooks" : [
153179 {
154180 "type" : "command" ,
155- "command" : script_path ,
181+ "command" : command ,
156182 }
157183 ],
158184 }
159185
160186 # Check if this hook is already present (avoid duplicates)
161- if not _hook_already_present (merged [event ], script_path ):
187+ if not _hook_already_present (merged [event ], command ):
162188 merged [event ].append (hook_config )
163189
164190 return merged
0 commit comments