deepwork 0.1.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 (41) hide show
  1. deepwork/__init__.py +25 -0
  2. deepwork/cli/__init__.py +1 -0
  3. deepwork/cli/install.py +290 -0
  4. deepwork/cli/main.py +25 -0
  5. deepwork/cli/sync.py +176 -0
  6. deepwork/core/__init__.py +1 -0
  7. deepwork/core/adapters.py +373 -0
  8. deepwork/core/detector.py +93 -0
  9. deepwork/core/generator.py +290 -0
  10. deepwork/core/hooks_syncer.py +206 -0
  11. deepwork/core/parser.py +310 -0
  12. deepwork/core/policy_parser.py +285 -0
  13. deepwork/hooks/__init__.py +1 -0
  14. deepwork/hooks/evaluate_policies.py +159 -0
  15. deepwork/schemas/__init__.py +1 -0
  16. deepwork/schemas/job_schema.py +212 -0
  17. deepwork/schemas/policy_schema.py +68 -0
  18. deepwork/standard_jobs/deepwork_jobs/job.yml +102 -0
  19. deepwork/standard_jobs/deepwork_jobs/steps/define.md +359 -0
  20. deepwork/standard_jobs/deepwork_jobs/steps/implement.md +435 -0
  21. deepwork/standard_jobs/deepwork_jobs/steps/refine.md +447 -0
  22. deepwork/standard_jobs/deepwork_policy/hooks/capture_work_tree.sh +26 -0
  23. deepwork/standard_jobs/deepwork_policy/hooks/get_changed_files.sh +30 -0
  24. deepwork/standard_jobs/deepwork_policy/hooks/global_hooks.yml +8 -0
  25. deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +72 -0
  26. deepwork/standard_jobs/deepwork_policy/hooks/user_prompt_submit.sh +17 -0
  27. deepwork/standard_jobs/deepwork_policy/job.yml +35 -0
  28. deepwork/standard_jobs/deepwork_policy/steps/define.md +174 -0
  29. deepwork/templates/__init__.py +1 -0
  30. deepwork/templates/claude/command-job-step.md.jinja +210 -0
  31. deepwork/templates/gemini/command-job-step.toml.jinja +169 -0
  32. deepwork/utils/__init__.py +1 -0
  33. deepwork/utils/fs.py +128 -0
  34. deepwork/utils/git.py +164 -0
  35. deepwork/utils/validation.py +31 -0
  36. deepwork/utils/yaml_utils.py +89 -0
  37. deepwork-0.1.0.dist-info/METADATA +389 -0
  38. deepwork-0.1.0.dist-info/RECORD +41 -0
  39. deepwork-0.1.0.dist-info/WHEEL +4 -0
  40. deepwork-0.1.0.dist-info/entry_points.txt +2 -0
  41. deepwork-0.1.0.dist-info/licenses/LICENSE.md +60 -0
@@ -0,0 +1,373 @@
1
+ """Agent adapters for AI coding assistants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from abc import ABC, abstractmethod
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Any, ClassVar
10
+
11
+
12
+ class AdapterError(Exception):
13
+ """Exception raised for adapter errors."""
14
+
15
+ pass
16
+
17
+
18
+ class CommandLifecycleHook(str, Enum):
19
+ """Generic command lifecycle hook events supported by DeepWork.
20
+
21
+ These represent hook points in the AI agent's command execution lifecycle.
22
+ Each adapter maps these generic names to platform-specific event names.
23
+ The enum values are the generic names used in job.yml files.
24
+ """
25
+
26
+ # Triggered after the agent finishes responding (before returning to user)
27
+ # Use for quality validation loops, output verification
28
+ AFTER_AGENT = "after_agent"
29
+
30
+ # Triggered before the agent uses a tool
31
+ # Use for tool-specific validation or pre-processing
32
+ BEFORE_TOOL = "before_tool"
33
+
34
+ # Triggered when the user submits a new prompt
35
+ # Use for session initialization, context setup
36
+ BEFORE_PROMPT = "before_prompt"
37
+
38
+
39
+ # List of all supported command lifecycle hooks
40
+ COMMAND_LIFECYCLE_HOOKS_SUPPORTED: list[CommandLifecycleHook] = list(CommandLifecycleHook)
41
+
42
+
43
+ class AgentAdapter(ABC):
44
+ """Base class for AI agent platform adapters.
45
+
46
+ Subclasses are automatically registered when defined, enabling dynamic
47
+ discovery of supported platforms.
48
+ """
49
+
50
+ # Class-level registry for auto-discovery
51
+ _registry: ClassVar[dict[str, type[AgentAdapter]]] = {}
52
+
53
+ # Platform configuration (subclasses define as class attributes)
54
+ name: ClassVar[str]
55
+ display_name: ClassVar[str]
56
+ config_dir: ClassVar[str]
57
+ commands_dir: ClassVar[str] = "commands"
58
+ command_template: ClassVar[str] = "command-job-step.md.jinja"
59
+
60
+ # Mapping from generic CommandLifecycleHook to platform-specific event names.
61
+ # Subclasses should override this to provide platform-specific mappings.
62
+ hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {}
63
+
64
+ def __init__(self, project_root: Path | str | None = None):
65
+ """
66
+ Initialize adapter with optional project root.
67
+
68
+ Args:
69
+ project_root: Path to project root directory
70
+ """
71
+ self.project_root = Path(project_root) if project_root else None
72
+
73
+ def __init_subclass__(cls, **kwargs: Any) -> None:
74
+ """Auto-register subclasses."""
75
+ super().__init_subclass__(**kwargs)
76
+ # Only register if the class has a name attribute set (not inherited default)
77
+ if "name" in cls.__dict__ and cls.name:
78
+ AgentAdapter._registry[cls.name] = cls
79
+
80
+ @classmethod
81
+ def get_all(cls) -> dict[str, type[AgentAdapter]]:
82
+ """
83
+ Return all registered adapter classes.
84
+
85
+ Returns:
86
+ Dict mapping adapter names to adapter classes
87
+ """
88
+ return cls._registry.copy()
89
+
90
+ @classmethod
91
+ def get(cls, name: str) -> type[AgentAdapter]:
92
+ """
93
+ Get adapter class by name.
94
+
95
+ Args:
96
+ name: Adapter name (e.g., "claude", "gemini", "copilot")
97
+
98
+ Returns:
99
+ Adapter class
100
+
101
+ Raises:
102
+ AdapterError: If adapter name is not registered
103
+ """
104
+ if name not in cls._registry:
105
+ raise AdapterError(
106
+ f"Unknown adapter '{name}'. Supported adapters: {', '.join(cls._registry.keys())}"
107
+ )
108
+ return cls._registry[name]
109
+
110
+ @classmethod
111
+ def list_names(cls) -> list[str]:
112
+ """
113
+ List all registered adapter names.
114
+
115
+ Returns:
116
+ List of adapter names
117
+ """
118
+ return list(cls._registry.keys())
119
+
120
+ def get_template_dir(self, templates_root: Path) -> Path:
121
+ """
122
+ Get the template directory for this adapter.
123
+
124
+ Args:
125
+ templates_root: Root directory containing platform templates
126
+
127
+ Returns:
128
+ Path to this adapter's template directory
129
+ """
130
+ return templates_root / self.name
131
+
132
+ def get_commands_dir(self, project_root: Path | None = None) -> Path:
133
+ """
134
+ Get the commands directory path.
135
+
136
+ Args:
137
+ project_root: Project root (uses instance's project_root if not provided)
138
+
139
+ Returns:
140
+ Path to commands directory
141
+
142
+ Raises:
143
+ AdapterError: If no project root specified
144
+ """
145
+ root = project_root or self.project_root
146
+ if not root:
147
+ raise AdapterError("No project root specified")
148
+ return root / self.config_dir / self.commands_dir
149
+
150
+ def get_command_filename(self, job_name: str, step_id: str) -> str:
151
+ """
152
+ Get the filename for a command.
153
+
154
+ Can be overridden for different file formats (e.g., TOML for Gemini).
155
+
156
+ Args:
157
+ job_name: Name of the job
158
+ step_id: ID of the step
159
+
160
+ Returns:
161
+ Command filename (e.g., "job_name.step_id.md")
162
+ """
163
+ return f"{job_name}.{step_id}.md"
164
+
165
+ def detect(self, project_root: Path | None = None) -> bool:
166
+ """
167
+ Check if this platform is available in the project.
168
+
169
+ Args:
170
+ project_root: Project root (uses instance's project_root if not provided)
171
+
172
+ Returns:
173
+ True if platform config directory exists
174
+ """
175
+ root = project_root or self.project_root
176
+ if not root:
177
+ return False
178
+ config_path = root / self.config_dir
179
+ return config_path.exists() and config_path.is_dir()
180
+
181
+ def get_platform_hook_name(self, hook: CommandLifecycleHook) -> str | None:
182
+ """
183
+ Get the platform-specific event name for a generic hook.
184
+
185
+ Args:
186
+ hook: Generic CommandLifecycleHook
187
+
188
+ Returns:
189
+ Platform-specific event name, or None if not supported
190
+ """
191
+ return self.hook_name_mapping.get(hook)
192
+
193
+ def supports_hook(self, hook: CommandLifecycleHook) -> bool:
194
+ """
195
+ Check if this adapter supports a specific hook.
196
+
197
+ Args:
198
+ hook: Generic CommandLifecycleHook
199
+
200
+ Returns:
201
+ True if the hook is supported
202
+ """
203
+ return hook in self.hook_name_mapping
204
+
205
+ @abstractmethod
206
+ def sync_hooks(self, project_path: Path, hooks: dict[str, list[dict[str, Any]]]) -> int:
207
+ """
208
+ Sync hooks to platform settings.
209
+
210
+ Args:
211
+ project_path: Path to project root
212
+ hooks: Dict mapping lifecycle events to hook configurations
213
+
214
+ Returns:
215
+ Number of hooks synced
216
+
217
+ Raises:
218
+ AdapterError: If sync fails
219
+ """
220
+ pass
221
+
222
+
223
+ def _hook_already_present(hooks: list[dict[str, Any]], script_path: str) -> bool:
224
+ """Check if a hook with the given script path is already in the list."""
225
+ for hook in hooks:
226
+ hook_list = hook.get("hooks", [])
227
+ for h in hook_list:
228
+ if h.get("command") == script_path:
229
+ return True
230
+ return False
231
+
232
+
233
+ # =============================================================================
234
+ # Platform Adapters
235
+ # =============================================================================
236
+ #
237
+ # Each adapter must define hook_name_mapping to indicate which hooks it supports.
238
+ # Use an empty dict {} for platforms that don't support command-level hooks.
239
+ #
240
+ # Hook support reviewed:
241
+ # - Claude Code: Full support (Stop, PreToolUse, UserPromptSubmit) - 2025-01
242
+ # - Gemini CLI: No command-level hooks (reviewed 2026-01-12)
243
+ # Gemini's hooks are global/project-level in settings.json, not per-command.
244
+ # TOML command files only support 'prompt' and 'description' fields.
245
+ # See: doc/platforms/gemini/hooks_system.md
246
+ # =============================================================================
247
+
248
+
249
+ class ClaudeAdapter(AgentAdapter):
250
+ """Adapter for Claude Code."""
251
+
252
+ name = "claude"
253
+ display_name = "Claude Code"
254
+ config_dir = ".claude"
255
+
256
+ # Claude Code uses PascalCase event names
257
+ hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {
258
+ CommandLifecycleHook.AFTER_AGENT: "Stop",
259
+ CommandLifecycleHook.BEFORE_TOOL: "PreToolUse",
260
+ CommandLifecycleHook.BEFORE_PROMPT: "UserPromptSubmit",
261
+ }
262
+
263
+ def sync_hooks(self, project_path: Path, hooks: dict[str, list[dict[str, Any]]]) -> int:
264
+ """
265
+ Sync hooks to Claude Code settings.json.
266
+
267
+ Args:
268
+ project_path: Path to project root
269
+ hooks: Merged hooks configuration
270
+
271
+ Returns:
272
+ Number of hooks synced
273
+
274
+ Raises:
275
+ AdapterError: If sync fails
276
+ """
277
+ if not hooks:
278
+ return 0
279
+
280
+ settings_file = project_path / self.config_dir / "settings.json"
281
+
282
+ # Load existing settings or create new
283
+ existing_settings: dict[str, Any] = {}
284
+ if settings_file.exists():
285
+ try:
286
+ with open(settings_file, encoding="utf-8") as f:
287
+ existing_settings = json.load(f)
288
+ except (json.JSONDecodeError, OSError) as e:
289
+ raise AdapterError(f"Failed to read settings.json: {e}") from e
290
+
291
+ # Merge hooks into existing settings
292
+ if "hooks" not in existing_settings:
293
+ existing_settings["hooks"] = {}
294
+
295
+ for event, event_hooks in hooks.items():
296
+ if event not in existing_settings["hooks"]:
297
+ existing_settings["hooks"][event] = []
298
+
299
+ # Add new hooks that aren't already present
300
+ for hook in event_hooks:
301
+ script_path = hook.get("hooks", [{}])[0].get("command", "")
302
+ if not _hook_already_present(existing_settings["hooks"][event], script_path):
303
+ existing_settings["hooks"][event].append(hook)
304
+
305
+ # Write back to settings.json
306
+ try:
307
+ settings_file.parent.mkdir(parents=True, exist_ok=True)
308
+ with open(settings_file, "w", encoding="utf-8") as f:
309
+ json.dump(existing_settings, f, indent=2)
310
+ except OSError as e:
311
+ raise AdapterError(f"Failed to write settings.json: {e}") from e
312
+
313
+ # Count total hooks
314
+ total = sum(len(hooks_list) for hooks_list in hooks.values())
315
+ return total
316
+
317
+
318
+ class GeminiAdapter(AgentAdapter):
319
+ """Adapter for Gemini CLI.
320
+
321
+ Gemini CLI uses TOML format for custom commands stored in .gemini/commands/.
322
+ Commands use colon (:) for namespacing instead of dot (.).
323
+
324
+ Note: Gemini CLI does NOT support command-level hooks. Hooks are configured
325
+ globally in settings.json, not per-command. Therefore, hook_name_mapping
326
+ is empty and sync_hooks returns 0.
327
+
328
+ See: doc/platforms/gemini/hooks_system.md
329
+ """
330
+
331
+ name = "gemini"
332
+ display_name = "Gemini CLI"
333
+ config_dir = ".gemini"
334
+ command_template = "command-job-step.toml.jinja"
335
+
336
+ # Gemini CLI does NOT support command-level hooks
337
+ # Hooks are global/project-level in settings.json, not per-command
338
+ hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {}
339
+
340
+ def get_command_filename(self, job_name: str, step_id: str) -> str:
341
+ """
342
+ Get the filename for a Gemini command.
343
+
344
+ Gemini uses TOML files and colon namespacing via subdirectories.
345
+ For job "my_job" and step "step_one", creates: my_job/step_one.toml
346
+
347
+ Args:
348
+ job_name: Name of the job
349
+ step_id: ID of the step
350
+
351
+ Returns:
352
+ Command filename path (e.g., "my_job/step_one.toml")
353
+ """
354
+ return f"{job_name}/{step_id}.toml"
355
+
356
+ def sync_hooks(self, project_path: Path, hooks: dict[str, list[dict[str, Any]]]) -> int:
357
+ """
358
+ Sync hooks to Gemini CLI settings.
359
+
360
+ Gemini CLI does not support command-level hooks. All hooks are
361
+ configured globally in settings.json. This method is a no-op
362
+ that always returns 0.
363
+
364
+ Args:
365
+ project_path: Path to project root
366
+ hooks: Dict mapping lifecycle events to hook configurations (ignored)
367
+
368
+ Returns:
369
+ 0 (Gemini does not support command-level hooks)
370
+ """
371
+ # Gemini CLI does not support command-level hooks
372
+ # Hooks are configured globally in settings.json, not per-command
373
+ return 0
@@ -0,0 +1,93 @@
1
+ """Platform detection for AI coding assistants."""
2
+
3
+ from pathlib import Path
4
+
5
+ from deepwork.core.adapters import AdapterError, AgentAdapter
6
+
7
+
8
+ class DetectorError(Exception):
9
+ """Exception raised for platform detection errors."""
10
+
11
+ pass
12
+
13
+
14
+ class PlatformDetector:
15
+ """Detects available AI coding platforms using registered adapters."""
16
+
17
+ def __init__(self, project_root: Path | str):
18
+ """
19
+ Initialize detector.
20
+
21
+ Args:
22
+ project_root: Path to project root directory
23
+ """
24
+ self.project_root = Path(project_root)
25
+
26
+ def detect_platform(self, platform_name: str) -> AgentAdapter | None:
27
+ """
28
+ Check if a specific platform is available.
29
+
30
+ Args:
31
+ platform_name: Platform name ("claude", "gemini", "copilot")
32
+
33
+ Returns:
34
+ AgentAdapter instance if platform is available, None otherwise
35
+
36
+ Raises:
37
+ DetectorError: If platform_name is not supported
38
+ """
39
+ try:
40
+ adapter_cls = AgentAdapter.get(platform_name)
41
+ except AdapterError as e:
42
+ raise DetectorError(str(e)) from e
43
+
44
+ adapter = adapter_cls(self.project_root)
45
+ if adapter.detect():
46
+ return adapter
47
+
48
+ return None
49
+
50
+ def detect_all_platforms(self) -> list[AgentAdapter]:
51
+ """
52
+ Detect all available platforms.
53
+
54
+ Returns:
55
+ List of available adapter instances
56
+ """
57
+ available = []
58
+ for platform_name in AgentAdapter.list_names():
59
+ adapter = self.detect_platform(platform_name)
60
+ if adapter is not None:
61
+ available.append(adapter)
62
+
63
+ return available
64
+
65
+ def get_adapter(self, platform_name: str) -> AgentAdapter:
66
+ """
67
+ Get an adapter instance for a platform (without checking availability).
68
+
69
+ Args:
70
+ platform_name: Platform name
71
+
72
+ Returns:
73
+ AgentAdapter instance
74
+
75
+ Raises:
76
+ DetectorError: If platform_name is not supported
77
+ """
78
+ try:
79
+ adapter_cls = AgentAdapter.get(platform_name)
80
+ except AdapterError as e:
81
+ raise DetectorError(str(e)) from e
82
+
83
+ return adapter_cls(self.project_root)
84
+
85
+ @staticmethod
86
+ def list_supported_platforms() -> list[str]:
87
+ """
88
+ List all supported platform names.
89
+
90
+ Returns:
91
+ List of platform names
92
+ """
93
+ return AgentAdapter.list_names()