Skip to content

Commit cf756dd

Browse files
committed
Cleanup hooks and wrappers
1 parent 78ed5d9 commit cf756dd

11 files changed

Lines changed: 224 additions & 228 deletions

File tree

.claude/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
"hooks": [
109109
{
110110
"type": "command",
111-
"command": ".deepwork/jobs/deepwork_rules/hooks/rules_stop_hook.sh"
111+
"command": "python -m deepwork.hooks.rules_check"
112112
}
113113
]
114114
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# DeepWork Rules Hooks Configuration
2-
# Maps Claude Code lifecycle events to hook scripts
2+
# Maps lifecycle events to hook scripts or Python modules
33

44
UserPromptSubmit:
55
- user_prompt_submit.sh
66

77
Stop:
8-
- rules_stop_hook.sh
8+
- module: deepwork.hooks.rules_check

.deepwork/jobs/deepwork_rules/hooks/rules_stop_hook.sh

Lines changed: 0 additions & 43 deletions
This file was deleted.

doc/architecture.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ deepwork/ # DeepWork tool repository
7373
│ │ └── hooks/ # Hook scripts
7474
│ │ ├── global_hooks.yml
7575
│ │ ├── user_prompt_submit.sh
76-
│ │ ├── capture_prompt_work_tree.sh
77-
│ │ └── rules_stop_hook.sh
76+
│ │ └── capture_prompt_work_tree.sh
7877
│ ├── schemas/ # Definition schemas
7978
│ │ ├── job_schema.py
8079
│ │ └── rules_schema.py
@@ -307,8 +306,7 @@ my-project/ # User's project (target)
307306
│ │ └── hooks/ # Hook scripts (installed from standard_jobs)
308307
│ │ ├── global_hooks.yml
309308
│ │ ├── user_prompt_submit.sh
310-
│ │ ├── capture_prompt_work_tree.sh
311-
│ │ └── rules_stop_hook.sh
309+
│ │ └── capture_prompt_work_tree.sh
312310
│ ├── competitive_research/
313311
│ │ ├── job.yml # Job metadata
314312
│ │ └── steps/
@@ -1135,7 +1133,7 @@ The hooks are installed to `.claude/settings.json` during `deepwork sync`:
11351133
{
11361134
"hooks": {
11371135
"Stop": [
1138-
{"matcher": "", "hooks": [{"type": "command", "command": ".deepwork/jobs/deepwork_rules/hooks/rules_stop_hook.sh"}]}
1136+
{"matcher": "", "hooks": [{"type": "command", "command": "python -m deepwork.hooks.rules_check"}]}
11391137
]
11401138
}
11411139
}

src/deepwork/core/hooks_syncer.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,42 @@ class HooksSyncError(Exception):
1919
class 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
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# DeepWork Rules Hooks Configuration
2-
# Maps Claude Code lifecycle events to hook scripts
2+
# Maps lifecycle events to hook scripts or Python modules
33

44
UserPromptSubmit:
55
- user_prompt_submit.sh
66

77
Stop:
8-
- rules_stop_hook.sh
8+
- module: deepwork.hooks.rules_check

src/deepwork/standard_jobs/deepwork_rules/hooks/rules_stop_hook.sh

Lines changed: 0 additions & 43 deletions
This file was deleted.

tests/shell_script_tests/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Shell Script Tests
22

3-
Automated tests for DeepWork shell scripts, with a focus on validating Claude Code hooks JSON response formats.
3+
Automated tests for DeepWork shell scripts and hooks, with a focus on validating Claude Code hooks JSON response formats.
44

5-
## Scripts Tested
5+
## Hooks and Scripts Tested
66

7-
| Script | Type | Description |
8-
|--------|------|-------------|
9-
| `rules_stop_hook.sh` | Stop Hook | Evaluates rules and blocks agent stop if rules are triggered |
7+
| Hook/Script | Type | Description |
8+
|-------------|------|-------------|
9+
| `deepwork.hooks.rules_check` | Stop Hook (Python) | Evaluates rules and blocks agent stop if rules are triggered |
1010
| `user_prompt_submit.sh` | UserPromptSubmit Hook | Captures work tree state when user submits a prompt |
1111
| `capture_prompt_work_tree.sh` | Helper | Records current git state for `compare_to: prompt` rules |
1212
| `make_new_job.sh` | Utility | Creates directory structure for new DeepWork jobs |
@@ -49,10 +49,10 @@ uv run pytest tests/shell_script_tests/ --cov=src/deepwork
4949
```
5050
tests/shell_script_tests/
5151
├── conftest.py # Shared fixtures and helpers
52+
├── test_hooks.py # Consolidated hook tests (JSON format, exit codes)
5253
├── test_rules_stop_hook.py # Stop hook blocking/allowing tests
5354
├── test_user_prompt_submit.py # Prompt submission hook tests
5455
├── test_capture_prompt_work_tree.py # Work tree capture tests
55-
├── test_hooks_json_format.py # JSON format validation tests
5656
└── test_make_new_job.py # Job directory creation tests
5757
```
5858

0 commit comments

Comments
 (0)