deepwork 0.2.0__py3-none-any.whl → 0.3.1__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.
- deepwork/cli/install.py +116 -71
- deepwork/cli/sync.py +20 -20
- deepwork/core/adapters.py +88 -51
- deepwork/core/command_executor.py +173 -0
- deepwork/core/generator.py +148 -31
- deepwork/core/hooks_syncer.py +51 -25
- deepwork/core/parser.py +8 -0
- deepwork/core/pattern_matcher.py +271 -0
- deepwork/core/rules_parser.py +559 -0
- deepwork/core/rules_queue.py +321 -0
- deepwork/hooks/README.md +181 -0
- deepwork/hooks/__init__.py +77 -1
- deepwork/hooks/claude_hook.sh +55 -0
- deepwork/hooks/gemini_hook.sh +55 -0
- deepwork/hooks/rules_check.py +700 -0
- deepwork/hooks/wrapper.py +363 -0
- deepwork/schemas/job_schema.py +14 -1
- deepwork/schemas/rules_schema.py +135 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +35 -53
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +9 -6
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +28 -26
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +2 -2
- deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +30 -0
- deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml +8 -0
- deepwork/standard_jobs/deepwork_rules/job.yml +47 -0
- deepwork/standard_jobs/deepwork_rules/rules/.gitkeep +13 -0
- deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example +10 -0
- deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example +10 -0
- deepwork/standard_jobs/deepwork_rules/rules/security-review.md.example +11 -0
- deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md +46 -0
- deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example +13 -0
- deepwork/standard_jobs/deepwork_rules/steps/define.md +249 -0
- deepwork/templates/claude/skill-job-meta.md.jinja +70 -0
- deepwork/templates/claude/skill-job-step.md.jinja +198 -0
- deepwork/templates/gemini/skill-job-meta.toml.jinja +76 -0
- deepwork/templates/gemini/skill-job-step.toml.jinja +147 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/METADATA +56 -25
- deepwork-0.3.1.dist-info/RECORD +62 -0
- deepwork/core/policy_parser.py +0 -295
- deepwork/hooks/evaluate_policies.py +0 -376
- deepwork/schemas/policy_schema.py +0 -78
- deepwork/standard_jobs/deepwork_policy/hooks/capture_prompt_work_tree.sh +0 -27
- deepwork/standard_jobs/deepwork_policy/hooks/global_hooks.yml +0 -8
- deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +0 -56
- deepwork/standard_jobs/deepwork_policy/job.yml +0 -35
- deepwork/standard_jobs/deepwork_policy/steps/define.md +0 -195
- deepwork/templates/claude/command-job-step.md.jinja +0 -210
- deepwork/templates/default_policy.yml +0 -53
- deepwork/templates/gemini/command-job-step.toml.jinja +0 -169
- deepwork-0.2.0.dist-info/RECORD +0 -49
- /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/user_prompt_submit.sh +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/WHEEL +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/entry_points.txt +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
deepwork/cli/install.py
CHANGED
|
@@ -73,9 +73,9 @@ def _inject_deepwork_jobs(jobs_dir: Path, project_path: Path) -> None:
|
|
|
73
73
|
_inject_standard_job("deepwork_jobs", jobs_dir, project_path)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def
|
|
76
|
+
def _inject_deepwork_rules(jobs_dir: Path, project_path: Path) -> None:
|
|
77
77
|
"""
|
|
78
|
-
Inject the
|
|
78
|
+
Inject the deepwork_rules job definition into the project.
|
|
79
79
|
|
|
80
80
|
Args:
|
|
81
81
|
jobs_dir: Path to .deepwork/jobs directory
|
|
@@ -84,7 +84,7 @@ def _inject_deepwork_policy(jobs_dir: Path, project_path: Path) -> None:
|
|
|
84
84
|
Raises:
|
|
85
85
|
InstallError: If injection fails
|
|
86
86
|
"""
|
|
87
|
-
_inject_standard_job("
|
|
87
|
+
_inject_standard_job("deepwork_rules", jobs_dir, project_path)
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def _create_deepwork_gitignore(deepwork_dir: Path) -> None:
|
|
@@ -98,7 +98,7 @@ def _create_deepwork_gitignore(deepwork_dir: Path) -> None:
|
|
|
98
98
|
"""
|
|
99
99
|
gitignore_path = deepwork_dir / ".gitignore"
|
|
100
100
|
gitignore_content = """# DeepWork temporary files
|
|
101
|
-
# These files are used for
|
|
101
|
+
# These files are used for rules evaluation during sessions
|
|
102
102
|
.last_work_tree
|
|
103
103
|
"""
|
|
104
104
|
|
|
@@ -113,44 +113,83 @@ def _create_deepwork_gitignore(deepwork_dir: Path) -> None:
|
|
|
113
113
|
gitignore_path.write_text(gitignore_content)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def
|
|
116
|
+
def _create_rules_directory(project_path: Path) -> bool:
|
|
117
117
|
"""
|
|
118
|
-
Create
|
|
118
|
+
Create the v2 rules directory structure with example templates.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Creates .deepwork/rules/ with example rule files that users can customize.
|
|
121
|
+
Only creates the directory if it doesn't already exist.
|
|
121
122
|
|
|
122
123
|
Args:
|
|
123
124
|
project_path: Path to the project root
|
|
124
125
|
|
|
125
126
|
Returns:
|
|
126
|
-
True if the
|
|
127
|
+
True if the directory was created, False if it already existed
|
|
127
128
|
"""
|
|
128
|
-
|
|
129
|
+
rules_dir = project_path / ".deepwork" / "rules"
|
|
129
130
|
|
|
130
|
-
if
|
|
131
|
+
if rules_dir.exists():
|
|
131
132
|
return False
|
|
132
133
|
|
|
133
|
-
#
|
|
134
|
-
|
|
134
|
+
# Create the rules directory
|
|
135
|
+
ensure_dir(rules_dir)
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
137
|
+
# Copy example rule templates from the deepwork_rules standard job
|
|
138
|
+
example_rules_dir = Path(__file__).parent.parent / "standard_jobs" / "deepwork_rules" / "rules"
|
|
139
|
+
|
|
140
|
+
if example_rules_dir.exists():
|
|
141
|
+
# Copy all .example files
|
|
142
|
+
for example_file in example_rules_dir.glob("*.md.example"):
|
|
143
|
+
dest_file = rules_dir / example_file.name
|
|
144
|
+
shutil.copy(example_file, dest_file)
|
|
145
|
+
|
|
146
|
+
# Create a README file explaining the rules system
|
|
147
|
+
readme_content = """# DeepWork Rules
|
|
148
|
+
|
|
149
|
+
Rules are automated guardrails that trigger when specific files change during
|
|
150
|
+
AI agent sessions. They help ensure documentation stays current, security reviews
|
|
151
|
+
happen, and team guidelines are followed.
|
|
152
|
+
|
|
153
|
+
## Getting Started
|
|
154
|
+
|
|
155
|
+
1. Copy an example file and rename it (remove the `.example` suffix):
|
|
156
|
+
```
|
|
157
|
+
cp readme-documentation.md.example readme-documentation.md
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
2. Edit the file to match your project's patterns
|
|
161
|
+
|
|
162
|
+
3. The rule will automatically trigger when matching files change
|
|
163
|
+
|
|
164
|
+
## Rule Format
|
|
165
|
+
|
|
166
|
+
Rules use YAML frontmatter in markdown files:
|
|
167
|
+
|
|
168
|
+
```markdown
|
|
169
|
+
---
|
|
170
|
+
name: Rule Name
|
|
171
|
+
trigger: "pattern/**/*"
|
|
172
|
+
safety: "optional/pattern"
|
|
173
|
+
---
|
|
174
|
+
Instructions in markdown here.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Detection Modes
|
|
178
|
+
|
|
179
|
+
- **trigger/safety**: Fire when trigger matches, unless safety also matches
|
|
180
|
+
- **set**: Bidirectional file correspondence (e.g., source + test)
|
|
181
|
+
- **pair**: Directional correspondence (e.g., API code -> docs)
|
|
182
|
+
|
|
183
|
+
## Documentation
|
|
184
|
+
|
|
185
|
+
See `doc/rules_syntax.md` in the DeepWork repository for full syntax documentation.
|
|
186
|
+
|
|
187
|
+
## Creating Rules Interactively
|
|
188
|
+
|
|
189
|
+
Use `/deepwork_rules.define` to create new rules with guidance.
|
|
152
190
|
"""
|
|
153
|
-
|
|
191
|
+
readme_path = rules_dir / "README.md"
|
|
192
|
+
readme_path.write_text(readme_content)
|
|
154
193
|
|
|
155
194
|
return True
|
|
156
195
|
|
|
@@ -216,8 +255,10 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
216
255
|
)
|
|
217
256
|
console.print(" [green]✓[/green] Git repository found")
|
|
218
257
|
|
|
219
|
-
# Step 2: Detect or validate platform
|
|
258
|
+
# Step 2: Detect or validate platform(s)
|
|
220
259
|
detector = PlatformDetector(project_path)
|
|
260
|
+
platforms_to_add: list[str] = []
|
|
261
|
+
detected_adapters: list[AgentAdapter] = []
|
|
221
262
|
|
|
222
263
|
if platform_name:
|
|
223
264
|
# User specified platform - check if it's available
|
|
@@ -234,34 +275,33 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
234
275
|
)
|
|
235
276
|
|
|
236
277
|
console.print(f" [green]✓[/green] {adapter.display_name} detected")
|
|
237
|
-
|
|
278
|
+
platforms_to_add = [adapter.name]
|
|
279
|
+
detected_adapters = [adapter]
|
|
238
280
|
else:
|
|
239
|
-
# Auto-detect
|
|
240
|
-
console.print("[yellow]→[/yellow] Auto-detecting AI
|
|
281
|
+
# Auto-detect all available platforms
|
|
282
|
+
console.print("[yellow]→[/yellow] Auto-detecting AI platforms...")
|
|
241
283
|
available_adapters = detector.detect_all_platforms()
|
|
242
284
|
|
|
243
285
|
if not available_adapters:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
console.print(f" [green]✓[/green] {adapter.display_name} detected")
|
|
264
|
-
platform_to_add = adapter.name
|
|
286
|
+
# No platforms detected - default to Claude Code
|
|
287
|
+
console.print(" [dim]•[/dim] No AI platform detected, defaulting to Claude Code")
|
|
288
|
+
|
|
289
|
+
# Create .claude directory
|
|
290
|
+
claude_dir = project_path / ".claude"
|
|
291
|
+
ensure_dir(claude_dir)
|
|
292
|
+
console.print(f" [green]✓[/green] Created {claude_dir.relative_to(project_path)}/")
|
|
293
|
+
|
|
294
|
+
# Get Claude adapter
|
|
295
|
+
claude_adapter_class = AgentAdapter.get("claude")
|
|
296
|
+
claude_adapter = claude_adapter_class(project_root=project_path)
|
|
297
|
+
platforms_to_add = [claude_adapter.name]
|
|
298
|
+
detected_adapters = [claude_adapter]
|
|
299
|
+
else:
|
|
300
|
+
# Add all detected platforms
|
|
301
|
+
for adapter in available_adapters:
|
|
302
|
+
console.print(f" [green]✓[/green] {adapter.display_name} detected")
|
|
303
|
+
platforms_to_add.append(adapter.name)
|
|
304
|
+
detected_adapters = available_adapters
|
|
265
305
|
|
|
266
306
|
# Step 3: Create .deepwork/ directory structure
|
|
267
307
|
console.print("[yellow]→[/yellow] Creating DeepWork directory structure...")
|
|
@@ -274,17 +314,17 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
274
314
|
# Step 3b: Inject standard jobs (core job definitions)
|
|
275
315
|
console.print("[yellow]→[/yellow] Installing core job definitions...")
|
|
276
316
|
_inject_deepwork_jobs(jobs_dir, project_path)
|
|
277
|
-
|
|
317
|
+
_inject_deepwork_rules(jobs_dir, project_path)
|
|
278
318
|
|
|
279
319
|
# Step 3c: Create .gitignore for temporary files
|
|
280
320
|
_create_deepwork_gitignore(deepwork_dir)
|
|
281
321
|
console.print(" [green]✓[/green] Created .deepwork/.gitignore")
|
|
282
322
|
|
|
283
|
-
# Step 3d: Create
|
|
284
|
-
if
|
|
285
|
-
console.print(" [green]✓[/green] Created .deepwork
|
|
323
|
+
# Step 3d: Create rules directory with v2 templates
|
|
324
|
+
if _create_rules_directory(project_path):
|
|
325
|
+
console.print(" [green]✓[/green] Created .deepwork/rules/ with example templates")
|
|
286
326
|
else:
|
|
287
|
-
console.print(" [dim]•[/dim] .deepwork
|
|
327
|
+
console.print(" [dim]•[/dim] .deepwork/rules/ already exists")
|
|
288
328
|
|
|
289
329
|
# Step 4: Load or create config.yml
|
|
290
330
|
console.print("[yellow]→[/yellow] Updating configuration...")
|
|
@@ -304,32 +344,37 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
304
344
|
if "platforms" not in config_data:
|
|
305
345
|
config_data["platforms"] = []
|
|
306
346
|
|
|
307
|
-
# Add platform if not already present
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
347
|
+
# Add each platform if not already present
|
|
348
|
+
added_platforms: list[str] = []
|
|
349
|
+
for i, platform in enumerate(platforms_to_add):
|
|
350
|
+
adapter = detected_adapters[i]
|
|
351
|
+
if platform not in config_data["platforms"]:
|
|
352
|
+
config_data["platforms"].append(platform)
|
|
353
|
+
added_platforms.append(adapter.display_name)
|
|
354
|
+
console.print(f" [green]✓[/green] Added {adapter.display_name} to platforms")
|
|
355
|
+
else:
|
|
356
|
+
console.print(f" [dim]•[/dim] {adapter.display_name} already configured")
|
|
313
357
|
|
|
314
358
|
save_yaml(config_file, config_data)
|
|
315
359
|
console.print(f" [green]✓[/green] Updated {config_file.relative_to(project_path)}")
|
|
316
360
|
|
|
317
|
-
# Step 5: Run sync to generate
|
|
361
|
+
# Step 5: Run sync to generate skills
|
|
318
362
|
console.print()
|
|
319
|
-
console.print("[yellow]→[/yellow] Running sync to generate
|
|
363
|
+
console.print("[yellow]→[/yellow] Running sync to generate skills...")
|
|
320
364
|
console.print()
|
|
321
365
|
|
|
322
|
-
from deepwork.cli.sync import
|
|
366
|
+
from deepwork.cli.sync import sync_skills
|
|
323
367
|
|
|
324
368
|
try:
|
|
325
|
-
|
|
369
|
+
sync_skills(project_path)
|
|
326
370
|
except Exception as e:
|
|
327
|
-
raise InstallError(f"Failed to sync
|
|
371
|
+
raise InstallError(f"Failed to sync skills: {e}") from e
|
|
328
372
|
|
|
329
373
|
# Success message
|
|
330
374
|
console.print()
|
|
375
|
+
platform_names = ", ".join(a.display_name for a in detected_adapters)
|
|
331
376
|
console.print(
|
|
332
|
-
f"[bold green]✓ DeepWork installed successfully for {
|
|
377
|
+
f"[bold green]✓ DeepWork installed successfully for {platform_names}![/bold green]"
|
|
333
378
|
)
|
|
334
379
|
console.print()
|
|
335
380
|
console.print("[bold]Next steps:[/bold]")
|
deepwork/cli/sync.py
CHANGED
|
@@ -7,7 +7,7 @@ from rich.console import Console
|
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
|
|
9
9
|
from deepwork.core.adapters import AgentAdapter
|
|
10
|
-
from deepwork.core.generator import
|
|
10
|
+
from deepwork.core.generator import SkillGenerator
|
|
11
11
|
from deepwork.core.hooks_syncer import collect_job_hooks, sync_hooks_to_platform
|
|
12
12
|
from deepwork.core.parser import parse_job_definition
|
|
13
13
|
from deepwork.utils.fs import ensure_dir
|
|
@@ -31,13 +31,13 @@ class SyncError(Exception):
|
|
|
31
31
|
)
|
|
32
32
|
def sync(path: Path) -> None:
|
|
33
33
|
"""
|
|
34
|
-
Sync DeepWork
|
|
34
|
+
Sync DeepWork skills to all configured platforms.
|
|
35
35
|
|
|
36
|
-
Regenerates all
|
|
36
|
+
Regenerates all skills for job steps and core skills based on
|
|
37
37
|
the current job definitions in .deepwork/jobs/.
|
|
38
38
|
"""
|
|
39
39
|
try:
|
|
40
|
-
|
|
40
|
+
sync_skills(path)
|
|
41
41
|
except SyncError as e:
|
|
42
42
|
console.print(f"[red]Error:[/red] {e}")
|
|
43
43
|
raise click.Abort() from e
|
|
@@ -46,9 +46,9 @@ def sync(path: Path) -> None:
|
|
|
46
46
|
raise
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def
|
|
49
|
+
def sync_skills(project_path: Path) -> None:
|
|
50
50
|
"""
|
|
51
|
-
Sync
|
|
51
|
+
Sync skills to all configured platforms.
|
|
52
52
|
|
|
53
53
|
Args:
|
|
54
54
|
project_path: Path to project directory
|
|
@@ -78,7 +78,7 @@ def sync_commands(project_path: Path) -> None:
|
|
|
78
78
|
"Run 'deepwork install --platform <platform>' to add a platform."
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
-
console.print("[bold cyan]Syncing DeepWork
|
|
81
|
+
console.print("[bold cyan]Syncing DeepWork Skills[/bold cyan]\n")
|
|
82
82
|
|
|
83
83
|
# Discover jobs
|
|
84
84
|
jobs_dir = deepwork_dir / "jobs"
|
|
@@ -115,8 +115,8 @@ def sync_commands(project_path: Path) -> None:
|
|
|
115
115
|
console.print(f"[yellow]→[/yellow] Found {len(job_hooks_list)} job(s) with hooks")
|
|
116
116
|
|
|
117
117
|
# Sync each platform
|
|
118
|
-
generator =
|
|
119
|
-
stats = {"platforms": 0, "
|
|
118
|
+
generator = SkillGenerator()
|
|
119
|
+
stats = {"platforms": 0, "skills": 0, "hooks": 0}
|
|
120
120
|
synced_adapters: list[AgentAdapter] = []
|
|
121
121
|
|
|
122
122
|
for platform_name in platforms:
|
|
@@ -130,19 +130,19 @@ def sync_commands(project_path: Path) -> None:
|
|
|
130
130
|
console.print(f"\n[yellow]→[/yellow] Syncing to {adapter.display_name}...")
|
|
131
131
|
|
|
132
132
|
platform_dir = project_path / adapter.config_dir
|
|
133
|
-
|
|
133
|
+
skills_dir = platform_dir / adapter.skills_dir
|
|
134
134
|
|
|
135
|
-
# Create
|
|
136
|
-
ensure_dir(
|
|
135
|
+
# Create skills directory
|
|
136
|
+
ensure_dir(skills_dir)
|
|
137
137
|
|
|
138
|
-
# Generate
|
|
138
|
+
# Generate skills for all jobs
|
|
139
139
|
if jobs:
|
|
140
|
-
console.print(" [dim]•[/dim] Generating
|
|
140
|
+
console.print(" [dim]•[/dim] Generating skills...")
|
|
141
141
|
for job in jobs:
|
|
142
142
|
try:
|
|
143
|
-
job_paths = generator.
|
|
144
|
-
stats["
|
|
145
|
-
console.print(f" [green]✓[/green] {job.name} ({len(job_paths)}
|
|
143
|
+
job_paths = generator.generate_all_skills(job, adapter, platform_dir)
|
|
144
|
+
stats["skills"] += len(job_paths)
|
|
145
|
+
console.print(f" [green]✓[/green] {job.name} ({len(job_paths)} skills)")
|
|
146
146
|
except Exception as e:
|
|
147
147
|
console.print(f" [red]✗[/red] Failed for {job.name}: {e}")
|
|
148
148
|
|
|
@@ -170,7 +170,7 @@ def sync_commands(project_path: Path) -> None:
|
|
|
170
170
|
table.add_column("Count", style="green")
|
|
171
171
|
|
|
172
172
|
table.add_row("Platforms synced", str(stats["platforms"]))
|
|
173
|
-
table.add_row("Total
|
|
173
|
+
table.add_row("Total skills", str(stats["skills"]))
|
|
174
174
|
if stats["hooks"] > 0:
|
|
175
175
|
table.add_row("Hooks synced", str(stats["hooks"]))
|
|
176
176
|
|
|
@@ -178,8 +178,8 @@ def sync_commands(project_path: Path) -> None:
|
|
|
178
178
|
console.print()
|
|
179
179
|
|
|
180
180
|
# Show reload instructions for each synced platform
|
|
181
|
-
if synced_adapters and stats["
|
|
182
|
-
console.print("[bold]To use the new
|
|
181
|
+
if synced_adapters and stats["skills"] > 0:
|
|
182
|
+
console.print("[bold]To use the new skills:[/bold]")
|
|
183
183
|
for adapter in synced_adapters:
|
|
184
184
|
console.print(f" [cyan]{adapter.display_name}:[/cyan] {adapter.reload_instructions}")
|
|
185
185
|
console.print()
|
deepwork/core/adapters.py
CHANGED
|
@@ -15,10 +15,10 @@ class AdapterError(Exception):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
19
|
-
"""Generic
|
|
18
|
+
class SkillLifecycleHook(str, Enum):
|
|
19
|
+
"""Generic skill lifecycle hook events supported by DeepWork.
|
|
20
20
|
|
|
21
|
-
These represent hook points in the AI agent's
|
|
21
|
+
These represent hook points in the AI agent's skill execution lifecycle.
|
|
22
22
|
Each adapter maps these generic names to platform-specific event names.
|
|
23
23
|
The enum values are the generic names used in job.yml files.
|
|
24
24
|
"""
|
|
@@ -36,8 +36,8 @@ class CommandLifecycleHook(str, Enum):
|
|
|
36
36
|
BEFORE_PROMPT = "before_prompt"
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
# List of all supported
|
|
40
|
-
|
|
39
|
+
# List of all supported skill lifecycle hooks
|
|
40
|
+
SKILL_LIFECYCLE_HOOKS_SUPPORTED: list[SkillLifecycleHook] = list(SkillLifecycleHook)
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class AgentAdapter(ABC):
|
|
@@ -54,18 +54,17 @@ class AgentAdapter(ABC):
|
|
|
54
54
|
name: ClassVar[str]
|
|
55
55
|
display_name: ClassVar[str]
|
|
56
56
|
config_dir: ClassVar[str]
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
skills_dir: ClassVar[str] = "skills"
|
|
58
|
+
skill_template: ClassVar[str] = "skill-job-step.md.jinja"
|
|
59
|
+
meta_skill_template: ClassVar[str] = "skill-job-meta.md.jinja"
|
|
59
60
|
|
|
60
|
-
# Instructions for reloading
|
|
61
|
+
# Instructions for reloading skills after sync (shown to users)
|
|
61
62
|
# Subclasses should override with platform-specific instructions.
|
|
62
|
-
reload_instructions: ClassVar[str] =
|
|
63
|
-
"Restart your AI assistant session to use the new commands."
|
|
64
|
-
)
|
|
63
|
+
reload_instructions: ClassVar[str] = "Restart your AI assistant session to use the new skills."
|
|
65
64
|
|
|
66
|
-
# Mapping from generic
|
|
65
|
+
# Mapping from generic SkillLifecycleHook to platform-specific event names.
|
|
67
66
|
# Subclasses should override this to provide platform-specific mappings.
|
|
68
|
-
hook_name_mapping: ClassVar[dict[
|
|
67
|
+
hook_name_mapping: ClassVar[dict[SkillLifecycleHook, str]] = {}
|
|
69
68
|
|
|
70
69
|
def __init__(self, project_root: Path | str | None = None):
|
|
71
70
|
"""
|
|
@@ -135,15 +134,15 @@ class AgentAdapter(ABC):
|
|
|
135
134
|
"""
|
|
136
135
|
return templates_root / self.name
|
|
137
136
|
|
|
138
|
-
def
|
|
137
|
+
def get_skills_dir(self, project_root: Path | None = None) -> Path:
|
|
139
138
|
"""
|
|
140
|
-
Get the
|
|
139
|
+
Get the skills directory path.
|
|
141
140
|
|
|
142
141
|
Args:
|
|
143
142
|
project_root: Project root (uses instance's project_root if not provided)
|
|
144
143
|
|
|
145
144
|
Returns:
|
|
146
|
-
Path to
|
|
145
|
+
Path to skills directory
|
|
147
146
|
|
|
148
147
|
Raises:
|
|
149
148
|
AdapterError: If no project root specified
|
|
@@ -151,22 +150,39 @@ class AgentAdapter(ABC):
|
|
|
151
150
|
root = project_root or self.project_root
|
|
152
151
|
if not root:
|
|
153
152
|
raise AdapterError("No project root specified")
|
|
154
|
-
return root / self.config_dir / self.
|
|
153
|
+
return root / self.config_dir / self.skills_dir
|
|
154
|
+
|
|
155
|
+
def get_meta_skill_filename(self, job_name: str) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Get the filename for a job's meta-skill.
|
|
158
|
+
|
|
159
|
+
The meta-skill is the primary user interface for a job.
|
|
160
|
+
Can be overridden for different file formats.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
job_name: Name of the job
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Meta-skill filename (e.g., "job_name/SKILL.md" for Claude)
|
|
167
|
+
"""
|
|
168
|
+
return f"{job_name}/SKILL.md"
|
|
155
169
|
|
|
156
|
-
def
|
|
170
|
+
def get_step_skill_filename(self, job_name: str, step_id: str, exposed: bool = False) -> str:
|
|
157
171
|
"""
|
|
158
|
-
Get the filename for a
|
|
172
|
+
Get the filename for a step skill.
|
|
159
173
|
|
|
160
|
-
|
|
174
|
+
All step skills use the same filename format. The exposed parameter
|
|
175
|
+
is used for template context (user-invocable frontmatter setting).
|
|
161
176
|
|
|
162
177
|
Args:
|
|
163
178
|
job_name: Name of the job
|
|
164
179
|
step_id: ID of the step
|
|
180
|
+
exposed: If True, skill is user-invocable (for template context). Default: False.
|
|
165
181
|
|
|
166
182
|
Returns:
|
|
167
|
-
|
|
183
|
+
Skill filename (e.g., "job_name.step_id/SKILL.md" for Claude)
|
|
168
184
|
"""
|
|
169
|
-
return f"{job_name}.{step_id}.md"
|
|
185
|
+
return f"{job_name}.{step_id}/SKILL.md"
|
|
170
186
|
|
|
171
187
|
def detect(self, project_root: Path | None = None) -> bool:
|
|
172
188
|
"""
|
|
@@ -184,24 +200,24 @@ class AgentAdapter(ABC):
|
|
|
184
200
|
config_path = root / self.config_dir
|
|
185
201
|
return config_path.exists() and config_path.is_dir()
|
|
186
202
|
|
|
187
|
-
def get_platform_hook_name(self, hook:
|
|
203
|
+
def get_platform_hook_name(self, hook: SkillLifecycleHook) -> str | None:
|
|
188
204
|
"""
|
|
189
205
|
Get the platform-specific event name for a generic hook.
|
|
190
206
|
|
|
191
207
|
Args:
|
|
192
|
-
hook: Generic
|
|
208
|
+
hook: Generic SkillLifecycleHook
|
|
193
209
|
|
|
194
210
|
Returns:
|
|
195
211
|
Platform-specific event name, or None if not supported
|
|
196
212
|
"""
|
|
197
213
|
return self.hook_name_mapping.get(hook)
|
|
198
214
|
|
|
199
|
-
def supports_hook(self, hook:
|
|
215
|
+
def supports_hook(self, hook: SkillLifecycleHook) -> bool:
|
|
200
216
|
"""
|
|
201
217
|
Check if this adapter supports a specific hook.
|
|
202
218
|
|
|
203
219
|
Args:
|
|
204
|
-
hook: Generic
|
|
220
|
+
hook: Generic SkillLifecycleHook
|
|
205
221
|
|
|
206
222
|
Returns:
|
|
207
223
|
True if the hook is supported
|
|
@@ -241,13 +257,15 @@ def _hook_already_present(hooks: list[dict[str, Any]], script_path: str) -> bool
|
|
|
241
257
|
# =============================================================================
|
|
242
258
|
#
|
|
243
259
|
# Each adapter must define hook_name_mapping to indicate which hooks it supports.
|
|
244
|
-
# Use an empty dict {} for platforms that don't support
|
|
260
|
+
# Use an empty dict {} for platforms that don't support skill-level hooks.
|
|
245
261
|
#
|
|
246
262
|
# Hook support reviewed:
|
|
247
|
-
# - Claude Code: Full support (Stop, PreToolUse, UserPromptSubmit) -
|
|
248
|
-
#
|
|
249
|
-
#
|
|
250
|
-
#
|
|
263
|
+
# - Claude Code: Full support (Stop, PreToolUse, UserPromptSubmit) - reviewed 2026-01-16
|
|
264
|
+
# All three skill lifecycle hooks are supported in markdown frontmatter.
|
|
265
|
+
# See: doc/platforms/claude/hooks_system.md
|
|
266
|
+
# - Gemini CLI: No skill-level hooks (reviewed 2026-01-12)
|
|
267
|
+
# Gemini's hooks are global/project-level in settings.json, not per-skill.
|
|
268
|
+
# TOML skill files only support 'prompt' and 'description' fields.
|
|
251
269
|
# See: doc/platforms/gemini/hooks_system.md
|
|
252
270
|
# =============================================================================
|
|
253
271
|
|
|
@@ -266,10 +284,10 @@ class ClaudeAdapter(AgentAdapter):
|
|
|
266
284
|
)
|
|
267
285
|
|
|
268
286
|
# Claude Code uses PascalCase event names
|
|
269
|
-
hook_name_mapping: ClassVar[dict[
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
287
|
+
hook_name_mapping: ClassVar[dict[SkillLifecycleHook, str]] = {
|
|
288
|
+
SkillLifecycleHook.AFTER_AGENT: "Stop",
|
|
289
|
+
SkillLifecycleHook.BEFORE_TOOL: "PreToolUse",
|
|
290
|
+
SkillLifecycleHook.BEFORE_PROMPT: "UserPromptSubmit",
|
|
273
291
|
}
|
|
274
292
|
|
|
275
293
|
def sync_hooks(self, project_path: Path, hooks: dict[str, list[dict[str, Any]]]) -> int:
|
|
@@ -330,11 +348,11 @@ class ClaudeAdapter(AgentAdapter):
|
|
|
330
348
|
class GeminiAdapter(AgentAdapter):
|
|
331
349
|
"""Adapter for Gemini CLI.
|
|
332
350
|
|
|
333
|
-
Gemini CLI uses TOML format for custom
|
|
334
|
-
|
|
351
|
+
Gemini CLI uses TOML format for custom skills stored in .gemini/skills/.
|
|
352
|
+
Skills use colon (:) for namespacing instead of dot (.).
|
|
335
353
|
|
|
336
|
-
Note: Gemini CLI does NOT support
|
|
337
|
-
globally in settings.json, not per-
|
|
354
|
+
Note: Gemini CLI does NOT support skill-level hooks. Hooks are configured
|
|
355
|
+
globally in settings.json, not per-skill. Therefore, hook_name_mapping
|
|
338
356
|
is empty and sync_hooks returns 0.
|
|
339
357
|
|
|
340
358
|
See: doc/platforms/gemini/hooks_system.md
|
|
@@ -343,30 +361,49 @@ class GeminiAdapter(AgentAdapter):
|
|
|
343
361
|
name = "gemini"
|
|
344
362
|
display_name = "Gemini CLI"
|
|
345
363
|
config_dir = ".gemini"
|
|
346
|
-
|
|
364
|
+
skill_template = "skill-job-step.toml.jinja"
|
|
365
|
+
meta_skill_template = "skill-job-meta.toml.jinja"
|
|
347
366
|
|
|
348
367
|
# Gemini CLI can reload with /memory refresh
|
|
349
368
|
reload_instructions: ClassVar[str] = (
|
|
350
|
-
"Run '/memory refresh' to reload
|
|
369
|
+
"Run '/memory refresh' to reload skills, or restart your Gemini CLI session."
|
|
351
370
|
)
|
|
352
371
|
|
|
353
|
-
# Gemini CLI does NOT support
|
|
354
|
-
# Hooks are global/project-level in settings.json, not per-
|
|
355
|
-
hook_name_mapping: ClassVar[dict[
|
|
372
|
+
# Gemini CLI does NOT support skill-level hooks
|
|
373
|
+
# Hooks are global/project-level in settings.json, not per-skill
|
|
374
|
+
hook_name_mapping: ClassVar[dict[SkillLifecycleHook, str]] = {}
|
|
375
|
+
|
|
376
|
+
def get_meta_skill_filename(self, job_name: str) -> str:
|
|
377
|
+
"""
|
|
378
|
+
Get the filename for a Gemini job's meta-skill.
|
|
379
|
+
|
|
380
|
+
Gemini uses TOML files and colon namespacing via subdirectories.
|
|
381
|
+
For job "my_job", creates: my_job/index.toml
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
job_name: Name of the job
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Meta-skill filename path (e.g., "my_job/index.toml")
|
|
388
|
+
"""
|
|
389
|
+
return f"{job_name}/index.toml"
|
|
356
390
|
|
|
357
|
-
def
|
|
391
|
+
def get_step_skill_filename(self, job_name: str, step_id: str, exposed: bool = False) -> str:
|
|
358
392
|
"""
|
|
359
|
-
Get the filename for a Gemini
|
|
393
|
+
Get the filename for a Gemini step skill.
|
|
360
394
|
|
|
361
395
|
Gemini uses TOML files and colon namespacing via subdirectories.
|
|
396
|
+
All step skills use the same filename format. The exposed parameter
|
|
397
|
+
is used for template context (user-invocable setting).
|
|
362
398
|
For job "my_job" and step "step_one", creates: my_job/step_one.toml
|
|
363
399
|
|
|
364
400
|
Args:
|
|
365
401
|
job_name: Name of the job
|
|
366
402
|
step_id: ID of the step
|
|
403
|
+
exposed: If True, skill is user-invocable (for template context). Default: False.
|
|
367
404
|
|
|
368
405
|
Returns:
|
|
369
|
-
|
|
406
|
+
Skill filename path (e.g., "my_job/step_one.toml")
|
|
370
407
|
"""
|
|
371
408
|
return f"{job_name}/{step_id}.toml"
|
|
372
409
|
|
|
@@ -374,7 +411,7 @@ class GeminiAdapter(AgentAdapter):
|
|
|
374
411
|
"""
|
|
375
412
|
Sync hooks to Gemini CLI settings.
|
|
376
413
|
|
|
377
|
-
Gemini CLI does not support
|
|
414
|
+
Gemini CLI does not support skill-level hooks. All hooks are
|
|
378
415
|
configured globally in settings.json. This method is a no-op
|
|
379
416
|
that always returns 0.
|
|
380
417
|
|
|
@@ -383,8 +420,8 @@ class GeminiAdapter(AgentAdapter):
|
|
|
383
420
|
hooks: Dict mapping lifecycle events to hook configurations (ignored)
|
|
384
421
|
|
|
385
422
|
Returns:
|
|
386
|
-
0 (Gemini does not support
|
|
423
|
+
0 (Gemini does not support skill-level hooks)
|
|
387
424
|
"""
|
|
388
|
-
# Gemini CLI does not support
|
|
389
|
-
# Hooks are configured globally in settings.json, not per-
|
|
425
|
+
# Gemini CLI does not support skill-level hooks
|
|
426
|
+
# Hooks are configured globally in settings.json, not per-skill
|
|
390
427
|
return 0
|