deepwork 0.1.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

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 (59) hide show
  1. deepwork/cli/install.py +121 -32
  2. deepwork/cli/sync.py +20 -20
  3. deepwork/core/adapters.py +88 -51
  4. deepwork/core/command_executor.py +173 -0
  5. deepwork/core/generator.py +148 -31
  6. deepwork/core/hooks_syncer.py +51 -25
  7. deepwork/core/parser.py +8 -0
  8. deepwork/core/pattern_matcher.py +271 -0
  9. deepwork/core/rules_parser.py +511 -0
  10. deepwork/core/rules_queue.py +321 -0
  11. deepwork/hooks/README.md +181 -0
  12. deepwork/hooks/__init__.py +77 -1
  13. deepwork/hooks/claude_hook.sh +55 -0
  14. deepwork/hooks/gemini_hook.sh +55 -0
  15. deepwork/hooks/rules_check.py +514 -0
  16. deepwork/hooks/wrapper.py +363 -0
  17. deepwork/schemas/job_schema.py +14 -1
  18. deepwork/schemas/rules_schema.py +103 -0
  19. deepwork/standard_jobs/deepwork_jobs/AGENTS.md +60 -0
  20. deepwork/standard_jobs/deepwork_jobs/job.yml +41 -56
  21. deepwork/standard_jobs/deepwork_jobs/make_new_job.sh +134 -0
  22. deepwork/standard_jobs/deepwork_jobs/steps/define.md +29 -63
  23. deepwork/standard_jobs/deepwork_jobs/steps/implement.md +62 -263
  24. deepwork/standard_jobs/deepwork_jobs/steps/learn.md +4 -62
  25. deepwork/standard_jobs/deepwork_jobs/templates/agents.md.template +32 -0
  26. deepwork/standard_jobs/deepwork_jobs/templates/job.yml.example +73 -0
  27. deepwork/standard_jobs/deepwork_jobs/templates/job.yml.template +56 -0
  28. deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.example +82 -0
  29. deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.template +58 -0
  30. deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml +8 -0
  31. deepwork/standard_jobs/deepwork_rules/job.yml +39 -0
  32. deepwork/standard_jobs/deepwork_rules/rules/.gitkeep +13 -0
  33. deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example +10 -0
  34. deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example +10 -0
  35. deepwork/standard_jobs/deepwork_rules/rules/security-review.md.example +11 -0
  36. deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md +45 -0
  37. deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example +13 -0
  38. deepwork/standard_jobs/deepwork_rules/steps/define.md +249 -0
  39. deepwork/templates/claude/skill-job-meta.md.jinja +70 -0
  40. deepwork/templates/claude/skill-job-step.md.jinja +198 -0
  41. deepwork/templates/gemini/skill-job-meta.toml.jinja +76 -0
  42. deepwork/templates/gemini/skill-job-step.toml.jinja +147 -0
  43. {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/METADATA +54 -24
  44. deepwork-0.3.0.dist-info/RECORD +62 -0
  45. deepwork/core/policy_parser.py +0 -295
  46. deepwork/hooks/evaluate_policies.py +0 -376
  47. deepwork/schemas/policy_schema.py +0 -78
  48. deepwork/standard_jobs/deepwork_policy/hooks/global_hooks.yml +0 -8
  49. deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +0 -56
  50. deepwork/standard_jobs/deepwork_policy/job.yml +0 -35
  51. deepwork/standard_jobs/deepwork_policy/steps/define.md +0 -195
  52. deepwork/templates/claude/command-job-step.md.jinja +0 -210
  53. deepwork/templates/gemini/command-job-step.toml.jinja +0 -169
  54. deepwork-0.1.1.dist-info/RECORD +0 -41
  55. /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/capture_prompt_work_tree.sh +0 -0
  56. /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/user_prompt_submit.sh +0 -0
  57. {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/WHEEL +0 -0
  58. {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/entry_points.txt +0 -0
  59. {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,321 @@
1
+ """Queue system for tracking rule state in .deepwork/tmp/rules/queue/."""
2
+
3
+ import hashlib
4
+ import json
5
+ from dataclasses import asdict, dataclass, field
6
+ from datetime import UTC, datetime
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ class QueueEntryStatus(Enum):
13
+ """Status of a queue entry."""
14
+
15
+ QUEUED = "queued" # Detected, awaiting evaluation
16
+ PASSED = "passed" # Evaluated, rule satisfied (promise found or action succeeded)
17
+ FAILED = "failed" # Evaluated, rule not satisfied
18
+ SKIPPED = "skipped" # Safety pattern matched, skipped
19
+
20
+
21
+ @dataclass
22
+ class ActionResult:
23
+ """Result of executing a rule action."""
24
+
25
+ type: str # "prompt" or "command"
26
+ output: str | None = None # Command stdout or prompt message shown
27
+ exit_code: int | None = None # Command exit code (None for prompt)
28
+
29
+
30
+ @dataclass
31
+ class QueueEntry:
32
+ """A single entry in the rules queue."""
33
+
34
+ # Identity
35
+ rule_name: str # Human-friendly name
36
+ rule_file: str # Filename (e.g., "source-test-pairing.md")
37
+ trigger_hash: str # Hash for deduplication
38
+
39
+ # State
40
+ status: QueueEntryStatus = QueueEntryStatus.QUEUED
41
+ created_at: str = "" # ISO8601 timestamp
42
+ evaluated_at: str | None = None # ISO8601 timestamp
43
+
44
+ # Context
45
+ baseline_ref: str = "" # Commit hash or timestamp used as baseline
46
+ trigger_files: list[str] = field(default_factory=list)
47
+ expected_files: list[str] = field(default_factory=list) # For set/pair modes
48
+ matched_files: list[str] = field(default_factory=list) # Files that also changed
49
+
50
+ # Result
51
+ action_result: ActionResult | None = None
52
+
53
+ def __post_init__(self) -> None:
54
+ if not self.created_at:
55
+ self.created_at = datetime.now(UTC).isoformat()
56
+
57
+ def to_dict(self) -> dict[str, Any]:
58
+ """Convert to dictionary for JSON serialization."""
59
+ data = asdict(self)
60
+ data["status"] = self.status.value
61
+ if self.action_result:
62
+ data["action_result"] = asdict(self.action_result)
63
+ return data
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: dict[str, Any]) -> "QueueEntry":
67
+ """Create from dictionary."""
68
+ action_result = None
69
+ if data.get("action_result"):
70
+ action_result = ActionResult(**data["action_result"])
71
+
72
+ return cls(
73
+ rule_name=data.get("rule_name", data.get("policy_name", "")),
74
+ rule_file=data.get("rule_file", data.get("policy_file", "")),
75
+ trigger_hash=data["trigger_hash"],
76
+ status=QueueEntryStatus(data["status"]),
77
+ created_at=data.get("created_at", ""),
78
+ evaluated_at=data.get("evaluated_at"),
79
+ baseline_ref=data.get("baseline_ref", ""),
80
+ trigger_files=data.get("trigger_files", []),
81
+ expected_files=data.get("expected_files", []),
82
+ matched_files=data.get("matched_files", []),
83
+ action_result=action_result,
84
+ )
85
+
86
+
87
+ def compute_trigger_hash(
88
+ rule_name: str,
89
+ trigger_files: list[str],
90
+ baseline_ref: str,
91
+ ) -> str:
92
+ """
93
+ Compute a hash for deduplication.
94
+
95
+ The hash is based on:
96
+ - Rule name
97
+ - Sorted list of trigger files
98
+ - Baseline reference (commit hash or timestamp)
99
+
100
+ Returns:
101
+ 12-character hex hash
102
+ """
103
+ hash_input = f"{rule_name}:{sorted(trigger_files)}:{baseline_ref}"
104
+ return hashlib.sha256(hash_input.encode()).hexdigest()[:12]
105
+
106
+
107
+ class RulesQueue:
108
+ """
109
+ Manages the rules queue in .deepwork/tmp/rules/queue/.
110
+
111
+ Queue entries are stored as JSON files named {hash}.{status}.json
112
+ """
113
+
114
+ def __init__(self, queue_dir: Path | None = None):
115
+ """
116
+ Initialize the queue.
117
+
118
+ Args:
119
+ queue_dir: Path to queue directory. Defaults to .deepwork/tmp/rules/queue/
120
+ """
121
+ if queue_dir is None:
122
+ queue_dir = Path(".deepwork/tmp/rules/queue")
123
+ self.queue_dir = queue_dir
124
+
125
+ def _ensure_dir(self) -> None:
126
+ """Ensure queue directory exists."""
127
+ self.queue_dir.mkdir(parents=True, exist_ok=True)
128
+
129
+ def _get_entry_path(self, trigger_hash: str, status: QueueEntryStatus) -> Path:
130
+ """Get path for an entry file."""
131
+ return self.queue_dir / f"{trigger_hash}.{status.value}.json"
132
+
133
+ def _find_entry_path(self, trigger_hash: str) -> Path | None:
134
+ """Find existing entry file for a hash (any status)."""
135
+ for status in QueueEntryStatus:
136
+ path = self._get_entry_path(trigger_hash, status)
137
+ if path.exists():
138
+ return path
139
+ return None
140
+
141
+ def has_entry(self, trigger_hash: str) -> bool:
142
+ """Check if an entry exists for this hash."""
143
+ return self._find_entry_path(trigger_hash) is not None
144
+
145
+ def get_entry(self, trigger_hash: str) -> QueueEntry | None:
146
+ """Get an entry by hash."""
147
+ path = self._find_entry_path(trigger_hash)
148
+ if path is None:
149
+ return None
150
+
151
+ try:
152
+ with open(path, encoding="utf-8") as f:
153
+ data = json.load(f)
154
+ return QueueEntry.from_dict(data)
155
+ except (json.JSONDecodeError, OSError, KeyError):
156
+ return None
157
+
158
+ def create_entry(
159
+ self,
160
+ rule_name: str,
161
+ rule_file: str,
162
+ trigger_files: list[str],
163
+ baseline_ref: str,
164
+ expected_files: list[str] | None = None,
165
+ ) -> QueueEntry | None:
166
+ """
167
+ Create a new queue entry if one doesn't already exist.
168
+
169
+ Args:
170
+ rule_name: Human-friendly rule name
171
+ rule_file: Rule filename (e.g., "source-test-pairing.md")
172
+ trigger_files: Files that triggered the rule
173
+ baseline_ref: Baseline reference for change detection
174
+ expected_files: Expected corresponding files (for set/pair)
175
+
176
+ Returns:
177
+ Created QueueEntry, or None if entry already exists
178
+ """
179
+ trigger_hash = compute_trigger_hash(rule_name, trigger_files, baseline_ref)
180
+
181
+ # Check if already exists
182
+ if self.has_entry(trigger_hash):
183
+ return None
184
+
185
+ self._ensure_dir()
186
+
187
+ entry = QueueEntry(
188
+ rule_name=rule_name,
189
+ rule_file=rule_file,
190
+ trigger_hash=trigger_hash,
191
+ status=QueueEntryStatus.QUEUED,
192
+ baseline_ref=baseline_ref,
193
+ trigger_files=trigger_files,
194
+ expected_files=expected_files or [],
195
+ )
196
+
197
+ path = self._get_entry_path(trigger_hash, QueueEntryStatus.QUEUED)
198
+ with open(path, "w", encoding="utf-8") as f:
199
+ json.dump(entry.to_dict(), f, indent=2)
200
+
201
+ return entry
202
+
203
+ def update_status(
204
+ self,
205
+ trigger_hash: str,
206
+ new_status: QueueEntryStatus,
207
+ action_result: ActionResult | None = None,
208
+ ) -> bool:
209
+ """
210
+ Update the status of an entry.
211
+
212
+ This renames the file to reflect the new status.
213
+
214
+ Args:
215
+ trigger_hash: Hash of the entry to update
216
+ new_status: New status
217
+ action_result: Optional result of action execution
218
+
219
+ Returns:
220
+ True if updated, False if entry not found
221
+ """
222
+ old_path = self._find_entry_path(trigger_hash)
223
+ if old_path is None:
224
+ return False
225
+
226
+ # Load existing entry
227
+ try:
228
+ with open(old_path, encoding="utf-8") as f:
229
+ data = json.load(f)
230
+ except (json.JSONDecodeError, OSError):
231
+ return False
232
+
233
+ # Update fields
234
+ data["status"] = new_status.value
235
+ data["evaluated_at"] = datetime.now(UTC).isoformat()
236
+ if action_result:
237
+ data["action_result"] = asdict(action_result)
238
+
239
+ # Write to new path
240
+ new_path = self._get_entry_path(trigger_hash, new_status)
241
+
242
+ # If status didn't change, just update in place
243
+ if old_path == new_path:
244
+ with open(new_path, "w", encoding="utf-8") as f:
245
+ json.dump(data, f, indent=2)
246
+ else:
247
+ # Write new file then delete old
248
+ with open(new_path, "w", encoding="utf-8") as f:
249
+ json.dump(data, f, indent=2)
250
+ old_path.unlink()
251
+
252
+ return True
253
+
254
+ def get_queued_entries(self) -> list[QueueEntry]:
255
+ """Get all entries with QUEUED status."""
256
+ if not self.queue_dir.exists():
257
+ return []
258
+
259
+ entries = []
260
+ for path in self.queue_dir.glob("*.queued.json"):
261
+ try:
262
+ with open(path, encoding="utf-8") as f:
263
+ data = json.load(f)
264
+ entries.append(QueueEntry.from_dict(data))
265
+ except (json.JSONDecodeError, OSError, KeyError):
266
+ continue
267
+
268
+ return entries
269
+
270
+ def get_all_entries(self) -> list[QueueEntry]:
271
+ """Get all entries regardless of status."""
272
+ if not self.queue_dir.exists():
273
+ return []
274
+
275
+ entries = []
276
+ for path in self.queue_dir.glob("*.json"):
277
+ try:
278
+ with open(path, encoding="utf-8") as f:
279
+ data = json.load(f)
280
+ entries.append(QueueEntry.from_dict(data))
281
+ except (json.JSONDecodeError, OSError, KeyError):
282
+ continue
283
+
284
+ return entries
285
+
286
+ def clear(self) -> int:
287
+ """
288
+ Clear all entries from the queue.
289
+
290
+ Returns:
291
+ Number of entries removed
292
+ """
293
+ if not self.queue_dir.exists():
294
+ return 0
295
+
296
+ count = 0
297
+ for path in self.queue_dir.glob("*.json"):
298
+ try:
299
+ path.unlink()
300
+ count += 1
301
+ except OSError:
302
+ continue
303
+
304
+ return count
305
+
306
+ def remove_entry(self, trigger_hash: str) -> bool:
307
+ """
308
+ Remove an entry by hash.
309
+
310
+ Returns:
311
+ True if removed, False if not found
312
+ """
313
+ path = self._find_entry_path(trigger_hash)
314
+ if path is None:
315
+ return False
316
+
317
+ try:
318
+ path.unlink()
319
+ return True
320
+ except OSError:
321
+ return False
@@ -0,0 +1,181 @@
1
+ # DeepWork Hooks
2
+
3
+ This directory contains the cross-platform hook system for DeepWork. Hooks allow validating and controlling AI agent behavior during execution.
4
+
5
+ ## Overview
6
+
7
+ The hook system provides:
8
+
9
+ 1. **Platform-specific shell wrappers** that normalize input/output:
10
+ - `claude_hook.sh` - For Claude Code
11
+ - `gemini_hook.sh` - For Gemini CLI
12
+
13
+ 2. **Common Python module** (`wrapper.py`) that handles:
14
+ - Input normalization (event names, tool names, JSON structure)
15
+ - Output denormalization (decision values, JSON structure)
16
+ - Cross-platform compatibility
17
+
18
+ 3. **Hook implementations**:
19
+ - `rules_check.py` - Evaluates DeepWork rules on `after_agent` events
20
+
21
+ ## Usage
22
+
23
+ ### Registering Hooks
24
+
25
+ #### Claude Code (`.claude/settings.json`)
26
+
27
+ ```json
28
+ {
29
+ "hooks": {
30
+ "Stop": [
31
+ {
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": "path/to/claude_hook.sh deepwork.hooks.rules_check"
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
41
+ }
42
+ ```
43
+
44
+ #### Gemini CLI (`.gemini/settings.json`)
45
+
46
+ ```json
47
+ {
48
+ "hooks": {
49
+ "AfterAgent": [
50
+ {
51
+ "hooks": [
52
+ {
53
+ "type": "command",
54
+ "command": "path/to/gemini_hook.sh deepwork.hooks.rules_check"
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Writing Custom Hooks
64
+
65
+ 1. Create a new Python module in `deepwork/hooks/`:
66
+
67
+ ```python
68
+ """my_custom_hook.py - Example custom hook."""
69
+
70
+ import os
71
+ import sys
72
+
73
+ from deepwork.hooks.wrapper import (
74
+ HookInput,
75
+ HookOutput,
76
+ NormalizedEvent,
77
+ Platform,
78
+ run_hook,
79
+ )
80
+
81
+
82
+ def my_hook(hook_input: HookInput) -> HookOutput:
83
+ """Hook logic that works on any platform."""
84
+
85
+ # Check the normalized event type
86
+ if hook_input.event == NormalizedEvent.AFTER_AGENT:
87
+ # Example: block if certain condition is met
88
+ if some_condition():
89
+ return HookOutput(
90
+ decision="block",
91
+ reason="Cannot complete until X is done"
92
+ )
93
+
94
+ elif hook_input.event == NormalizedEvent.BEFORE_TOOL:
95
+ # Example: validate tool usage
96
+ if hook_input.tool_name == "write_file":
97
+ file_path = hook_input.tool_input.get("file_path", "")
98
+ if "/secrets/" in file_path:
99
+ return HookOutput(
100
+ decision="deny",
101
+ reason="Cannot write to secrets directory"
102
+ )
103
+
104
+ # Allow the action
105
+ return HookOutput()
106
+
107
+
108
+ def main() -> None:
109
+ """Entry point called by shell wrappers."""
110
+ platform_str = os.environ.get("DEEPWORK_HOOK_PLATFORM", "claude")
111
+ try:
112
+ platform = Platform(platform_str)
113
+ except ValueError:
114
+ platform = Platform.CLAUDE
115
+
116
+ exit_code = run_hook(my_hook, platform)
117
+ sys.exit(exit_code)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
122
+ ```
123
+
124
+ 2. Register the hook using the appropriate shell wrapper.
125
+
126
+ ## Event Mapping
127
+
128
+ | DeepWork Normalized | Claude Code | Gemini CLI |
129
+ |---------------------|-------------|------------|
130
+ | `after_agent` | Stop | AfterAgent |
131
+ | `before_tool` | PreToolUse | BeforeTool |
132
+ | `after_tool` | PostToolUse | AfterTool |
133
+ | `before_prompt` | UserPromptSubmit | BeforeAgent |
134
+ | `session_start` | SessionStart | SessionStart |
135
+ | `session_end` | SessionEnd | SessionEnd |
136
+
137
+ ## Tool Name Mapping
138
+
139
+ | Normalized | Claude Code | Gemini CLI |
140
+ |------------|-------------|------------|
141
+ | `write_file` | Write | write_file |
142
+ | `read_file` | Read | read_file |
143
+ | `edit_file` | Edit | edit_file |
144
+ | `shell` | Bash | shell |
145
+ | `glob` | Glob | glob |
146
+ | `grep` | Grep | grep |
147
+
148
+ ## Decision Values
149
+
150
+ | Effect | Claude Code | Gemini CLI |
151
+ |--------|-------------|------------|
152
+ | Block action | `"block"` | `"deny"` (auto-converted) |
153
+ | Allow action | `"allow"` or `{}` | `"allow"` or `{}` |
154
+ | Deny tool use | `"deny"` | `"deny"` |
155
+
156
+ The wrapper automatically converts `"block"` to `"deny"` for Gemini CLI.
157
+
158
+ ## Exit Codes
159
+
160
+ | Code | Meaning |
161
+ |------|---------|
162
+ | 0 | Success (allow action) |
163
+ | 2 | Blocking error (prevent action) |
164
+
165
+ ## Testing
166
+
167
+ Run the hook wrapper tests:
168
+
169
+ ```bash
170
+ pytest tests/unit/test_hook_wrapper.py -v
171
+ pytest tests/shell_script_tests/test_hook_wrappers.py -v
172
+ ```
173
+
174
+ ## Files
175
+
176
+ | File | Purpose |
177
+ |------|---------|
178
+ | `wrapper.py` | Cross-platform input/output normalization |
179
+ | `claude_hook.sh` | Shell wrapper for Claude Code |
180
+ | `gemini_hook.sh` | Shell wrapper for Gemini CLI |
181
+ | `rules_check.py` | Cross-platform rule evaluation hook |
@@ -1 +1,77 @@
1
- """DeepWork hooks package for policy enforcement and lifecycle events."""
1
+ """DeepWork hooks package for rules enforcement and lifecycle events.
2
+
3
+ This package provides:
4
+
5
+ 1. Cross-platform hook wrapper system:
6
+ - wrapper.py: Normalizes input/output between Claude Code and Gemini CLI
7
+ - claude_hook.sh: Shell wrapper for Claude Code hooks
8
+ - gemini_hook.sh: Shell wrapper for Gemini CLI hooks
9
+
10
+ 2. Hook implementations:
11
+ - rules_check.py: Evaluates rules on after_agent events
12
+
13
+ Usage with wrapper system:
14
+ # Register hook in .claude/settings.json:
15
+ {
16
+ "hooks": {
17
+ "Stop": [{
18
+ "hooks": [{
19
+ "type": "command",
20
+ "command": ".deepwork/hooks/claude_hook.sh deepwork.hooks.rules_check"
21
+ }]
22
+ }]
23
+ }
24
+ }
25
+
26
+ # Register hook in .gemini/settings.json:
27
+ {
28
+ "hooks": {
29
+ "AfterAgent": [{
30
+ "hooks": [{
31
+ "type": "command",
32
+ "command": ".gemini/hooks/gemini_hook.sh deepwork.hooks.rules_check"
33
+ }]
34
+ }]
35
+ }
36
+ }
37
+
38
+ Writing custom hooks:
39
+ from deepwork.hooks.wrapper import (
40
+ HookInput,
41
+ HookOutput,
42
+ NormalizedEvent,
43
+ Platform,
44
+ run_hook,
45
+ )
46
+
47
+ def my_hook(input: HookInput) -> HookOutput:
48
+ if input.event == NormalizedEvent.AFTER_AGENT:
49
+ if should_block():
50
+ return HookOutput(decision="block", reason="Complete X first")
51
+ return HookOutput()
52
+
53
+ if __name__ == "__main__":
54
+ import os, sys
55
+ platform = Platform(os.environ.get("DEEPWORK_HOOK_PLATFORM", "claude"))
56
+ sys.exit(run_hook(my_hook, platform))
57
+ """
58
+
59
+ from deepwork.hooks.wrapper import (
60
+ HookInput,
61
+ HookOutput,
62
+ NormalizedEvent,
63
+ Platform,
64
+ denormalize_output,
65
+ normalize_input,
66
+ run_hook,
67
+ )
68
+
69
+ __all__ = [
70
+ "HookInput",
71
+ "HookOutput",
72
+ "NormalizedEvent",
73
+ "Platform",
74
+ "normalize_input",
75
+ "denormalize_output",
76
+ "run_hook",
77
+ ]
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # claude_hook.sh - Claude Code hook wrapper
3
+ #
4
+ # This script wraps Python hooks to work with Claude Code's hook system.
5
+ # It handles input/output normalization so Python hooks can be written once
6
+ # and work on any supported platform.
7
+ #
8
+ # Usage:
9
+ # claude_hook.sh <python_hook_module>
10
+ #
11
+ # Example:
12
+ # claude_hook.sh deepwork.hooks.rules_check
13
+ #
14
+ # The Python module should implement a main() function that:
15
+ # 1. Calls deepwork.hooks.wrapper.run_hook() with a hook function
16
+ # 2. The hook function receives HookInput and returns HookOutput
17
+ #
18
+ # Environment variables set by Claude Code:
19
+ # CLAUDE_PROJECT_DIR - Absolute path to project root
20
+ #
21
+ # Input (stdin): JSON from Claude Code hook system
22
+ # Output (stdout): JSON response for Claude Code
23
+ # Exit codes:
24
+ # 0 - Success (allow action)
25
+ # 2 - Blocking error (prevent action)
26
+
27
+ set -e
28
+
29
+ # Get the Python module to run
30
+ PYTHON_MODULE="${1:-}"
31
+
32
+ if [ -z "${PYTHON_MODULE}" ]; then
33
+ echo "Usage: claude_hook.sh <python_hook_module>" >&2
34
+ echo "Example: claude_hook.sh deepwork.hooks.rules_check" >&2
35
+ exit 1
36
+ fi
37
+
38
+ # Read stdin into variable
39
+ HOOK_INPUT=""
40
+ if [ ! -t 0 ]; then
41
+ HOOK_INPUT=$(cat)
42
+ fi
43
+
44
+ # Set platform environment variable for the Python module
45
+ export DEEPWORK_HOOK_PLATFORM="claude"
46
+
47
+ # Run the Python module, passing the input via stdin
48
+ # The Python module is responsible for:
49
+ # 1. Reading stdin (normalized by wrapper)
50
+ # 2. Processing the hook logic
51
+ # 3. Writing JSON to stdout
52
+ echo "${HOOK_INPUT}" | python -m "${PYTHON_MODULE}"
53
+ exit_code=$?
54
+
55
+ exit ${exit_code}
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # gemini_hook.sh - Gemini CLI hook wrapper
3
+ #
4
+ # This script wraps Python hooks to work with Gemini CLI's hook system.
5
+ # It handles input/output normalization so Python hooks can be written once
6
+ # and work on any supported platform.
7
+ #
8
+ # Usage:
9
+ # gemini_hook.sh <python_hook_module>
10
+ #
11
+ # Example:
12
+ # gemini_hook.sh deepwork.hooks.rules_check
13
+ #
14
+ # The Python module should implement a main() function that:
15
+ # 1. Calls deepwork.hooks.wrapper.run_hook() with a hook function
16
+ # 2. The hook function receives HookInput and returns HookOutput
17
+ #
18
+ # Environment variables set by Gemini CLI:
19
+ # GEMINI_PROJECT_DIR - Absolute path to project root
20
+ #
21
+ # Input (stdin): JSON from Gemini CLI hook system
22
+ # Output (stdout): JSON response for Gemini CLI
23
+ # Exit codes:
24
+ # 0 - Success (allow action)
25
+ # 2 - Blocking error (prevent action)
26
+
27
+ set -e
28
+
29
+ # Get the Python module to run
30
+ PYTHON_MODULE="${1:-}"
31
+
32
+ if [ -z "${PYTHON_MODULE}" ]; then
33
+ echo "Usage: gemini_hook.sh <python_hook_module>" >&2
34
+ echo "Example: gemini_hook.sh deepwork.hooks.rules_check" >&2
35
+ exit 1
36
+ fi
37
+
38
+ # Read stdin into variable
39
+ HOOK_INPUT=""
40
+ if [ ! -t 0 ]; then
41
+ HOOK_INPUT=$(cat)
42
+ fi
43
+
44
+ # Set platform environment variable for the Python module
45
+ export DEEPWORK_HOOK_PLATFORM="gemini"
46
+
47
+ # Run the Python module, passing the input via stdin
48
+ # The Python module is responsible for:
49
+ # 1. Reading stdin (normalized by wrapper)
50
+ # 2. Processing the hook logic
51
+ # 3. Writing JSON to stdout
52
+ echo "${HOOK_INPUT}" | python -m "${PYTHON_MODULE}"
53
+ exit_code=$?
54
+
55
+ exit ${exit_code}