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
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 _inject_deepwork_policy(jobs_dir: Path, project_path: Path) -> None:
76
+ def _inject_deepwork_rules(jobs_dir: Path, project_path: Path) -> None:
77
77
  """
78
- Inject the deepwork_policy job definition into the project.
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("deepwork_policy", jobs_dir, project_path)
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 policy evaluation during sessions
101
+ # These files are used for rules evaluation during sessions
102
102
  .last_work_tree
103
103
  """
104
104
 
@@ -113,6 +113,87 @@ def _create_deepwork_gitignore(deepwork_dir: Path) -> None:
113
113
  gitignore_path.write_text(gitignore_content)
114
114
 
115
115
 
116
+ def _create_rules_directory(project_path: Path) -> bool:
117
+ """
118
+ Create the v2 rules directory structure with example templates.
119
+
120
+ Creates .deepwork/rules/ with example rule files that users can customize.
121
+ Only creates the directory if it doesn't already exist.
122
+
123
+ Args:
124
+ project_path: Path to the project root
125
+
126
+ Returns:
127
+ True if the directory was created, False if it already existed
128
+ """
129
+ rules_dir = project_path / ".deepwork" / "rules"
130
+
131
+ if rules_dir.exists():
132
+ return False
133
+
134
+ # Create the rules directory
135
+ ensure_dir(rules_dir)
136
+
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.
190
+ """
191
+ readme_path = rules_dir / "README.md"
192
+ readme_path.write_text(readme_content)
193
+
194
+ return True
195
+
196
+
116
197
  class DynamicChoice(click.Choice):
117
198
  """A Click Choice that gets its values dynamically from AgentAdapter."""
118
199
 
@@ -174,8 +255,10 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
174
255
  )
175
256
  console.print(" [green]✓[/green] Git repository found")
176
257
 
177
- # Step 2: Detect or validate platform
258
+ # Step 2: Detect or validate platform(s)
178
259
  detector = PlatformDetector(project_path)
260
+ platforms_to_add: list[str] = []
261
+ detected_adapters: list[AgentAdapter] = []
179
262
 
180
263
  if platform_name:
181
264
  # User specified platform - check if it's available
@@ -192,10 +275,11 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
192
275
  )
193
276
 
194
277
  console.print(f" [green]✓[/green] {adapter.display_name} detected")
195
- platform_to_add = adapter.name
278
+ platforms_to_add = [adapter.name]
279
+ detected_adapters = [adapter]
196
280
  else:
197
- # Auto-detect platform
198
- console.print("[yellow]→[/yellow] Auto-detecting AI platform...")
281
+ # Auto-detect all available platforms
282
+ console.print("[yellow]→[/yellow] Auto-detecting AI platforms...")
199
283
  available_adapters = detector.detect_all_platforms()
200
284
 
201
285
  if not available_adapters:
@@ -209,17 +293,11 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
209
293
  "Please set up one of these platforms first, or use --platform to specify."
210
294
  )
211
295
 
212
- if len(available_adapters) > 1:
213
- # Multiple platforms - ask user to specify
214
- platform_names = ", ".join(a.display_name for a in available_adapters)
215
- raise InstallError(
216
- f"Multiple AI platforms detected: {platform_names}\n"
217
- "Please specify which platform to use with --platform option."
218
- )
219
-
220
- adapter = available_adapters[0]
221
- console.print(f" [green]✓[/green] {adapter.display_name} detected")
222
- platform_to_add = adapter.name
296
+ # Add all detected platforms
297
+ for adapter in available_adapters:
298
+ console.print(f" [green]✓[/green] {adapter.display_name} detected")
299
+ platforms_to_add.append(adapter.name)
300
+ detected_adapters = available_adapters
223
301
 
224
302
  # Step 3: Create .deepwork/ directory structure
225
303
  console.print("[yellow]→[/yellow] Creating DeepWork directory structure...")
@@ -232,12 +310,18 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
232
310
  # Step 3b: Inject standard jobs (core job definitions)
233
311
  console.print("[yellow]→[/yellow] Installing core job definitions...")
234
312
  _inject_deepwork_jobs(jobs_dir, project_path)
235
- _inject_deepwork_policy(jobs_dir, project_path)
313
+ _inject_deepwork_rules(jobs_dir, project_path)
236
314
 
237
315
  # Step 3c: Create .gitignore for temporary files
238
316
  _create_deepwork_gitignore(deepwork_dir)
239
317
  console.print(" [green]✓[/green] Created .deepwork/.gitignore")
240
318
 
319
+ # Step 3d: Create rules directory with v2 templates
320
+ if _create_rules_directory(project_path):
321
+ console.print(" [green]✓[/green] Created .deepwork/rules/ with example templates")
322
+ else:
323
+ console.print(" [dim]•[/dim] .deepwork/rules/ already exists")
324
+
241
325
  # Step 4: Load or create config.yml
242
326
  console.print("[yellow]→[/yellow] Updating configuration...")
243
327
  config_file = deepwork_dir / "config.yml"
@@ -256,32 +340,37 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
256
340
  if "platforms" not in config_data:
257
341
  config_data["platforms"] = []
258
342
 
259
- # Add platform if not already present
260
- if platform_to_add not in config_data["platforms"]:
261
- config_data["platforms"].append(platform_to_add)
262
- console.print(f" [green]✓[/green] Added {adapter.display_name} to platforms")
263
- else:
264
- console.print(f" [dim]•[/dim] {adapter.display_name} already configured")
343
+ # Add each platform if not already present
344
+ added_platforms: list[str] = []
345
+ for i, platform in enumerate(platforms_to_add):
346
+ adapter = detected_adapters[i]
347
+ if platform not in config_data["platforms"]:
348
+ config_data["platforms"].append(platform)
349
+ added_platforms.append(adapter.display_name)
350
+ console.print(f" [green]✓[/green] Added {adapter.display_name} to platforms")
351
+ else:
352
+ console.print(f" [dim]•[/dim] {adapter.display_name} already configured")
265
353
 
266
354
  save_yaml(config_file, config_data)
267
355
  console.print(f" [green]✓[/green] Updated {config_file.relative_to(project_path)}")
268
356
 
269
- # Step 5: Run sync to generate commands
357
+ # Step 5: Run sync to generate skills
270
358
  console.print()
271
- console.print("[yellow]→[/yellow] Running sync to generate commands...")
359
+ console.print("[yellow]→[/yellow] Running sync to generate skills...")
272
360
  console.print()
273
361
 
274
- from deepwork.cli.sync import sync_commands
362
+ from deepwork.cli.sync import sync_skills
275
363
 
276
364
  try:
277
- sync_commands(project_path)
365
+ sync_skills(project_path)
278
366
  except Exception as e:
279
- raise InstallError(f"Failed to sync commands: {e}") from e
367
+ raise InstallError(f"Failed to sync skills: {e}") from e
280
368
 
281
369
  # Success message
282
370
  console.print()
371
+ platform_names = ", ".join(a.display_name for a in detected_adapters)
283
372
  console.print(
284
- f"[bold green]✓ DeepWork installed successfully for {adapter.display_name}![/bold green]"
373
+ f"[bold green]✓ DeepWork installed successfully for {platform_names}![/bold green]"
285
374
  )
286
375
  console.print()
287
376
  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 CommandGenerator
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 commands to all configured platforms.
34
+ Sync DeepWork skills to all configured platforms.
35
35
 
36
- Regenerates all slash-commands for job steps and core commands based on
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
- sync_commands(path)
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 sync_commands(project_path: Path) -> None:
49
+ def sync_skills(project_path: Path) -> None:
50
50
  """
51
- Sync commands to all configured platforms.
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 Commands[/bold cyan]\n")
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 = CommandGenerator()
119
- stats = {"platforms": 0, "commands": 0, "hooks": 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
- commands_dir = platform_dir / adapter.commands_dir
133
+ skills_dir = platform_dir / adapter.skills_dir
134
134
 
135
- # Create commands directory
136
- ensure_dir(commands_dir)
135
+ # Create skills directory
136
+ ensure_dir(skills_dir)
137
137
 
138
- # Generate commands for all jobs
138
+ # Generate skills for all jobs
139
139
  if jobs:
140
- console.print(" [dim]•[/dim] Generating commands...")
140
+ console.print(" [dim]•[/dim] Generating skills...")
141
141
  for job in jobs:
142
142
  try:
143
- job_paths = generator.generate_all_commands(job, adapter, platform_dir)
144
- stats["commands"] += len(job_paths)
145
- console.print(f" [green]✓[/green] {job.name} ({len(job_paths)} commands)")
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 commands", str(stats["commands"]))
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["commands"] > 0:
182
- console.print("[bold]To use the new commands:[/bold]")
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 CommandLifecycleHook(str, Enum):
19
- """Generic command lifecycle hook events supported by DeepWork.
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 command execution lifecycle.
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 command lifecycle hooks
40
- COMMAND_LIFECYCLE_HOOKS_SUPPORTED: list[CommandLifecycleHook] = list(CommandLifecycleHook)
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
- commands_dir: ClassVar[str] = "commands"
58
- command_template: ClassVar[str] = "command-job-step.md.jinja"
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 commands after sync (shown to users)
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 CommandLifecycleHook to platform-specific event names.
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[CommandLifecycleHook, str]] = {}
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 get_commands_dir(self, project_root: Path | None = None) -> Path:
137
+ def get_skills_dir(self, project_root: Path | None = None) -> Path:
139
138
  """
140
- Get the commands directory path.
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 commands directory
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.commands_dir
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 get_command_filename(self, job_name: str, step_id: str) -> str:
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 command.
172
+ Get the filename for a step skill.
159
173
 
160
- Can be overridden for different file formats (e.g., TOML for Gemini).
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
- Command filename (e.g., "job_name.step_id.md")
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: CommandLifecycleHook) -> str | None:
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 CommandLifecycleHook
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: CommandLifecycleHook) -> bool:
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 CommandLifecycleHook
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 command-level hooks.
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) - 2025-01
248
- # - Gemini CLI: No command-level hooks (reviewed 2026-01-12)
249
- # Gemini's hooks are global/project-level in settings.json, not per-command.
250
- # TOML command files only support 'prompt' and 'description' fields.
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[CommandLifecycleHook, str]] = {
270
- CommandLifecycleHook.AFTER_AGENT: "Stop",
271
- CommandLifecycleHook.BEFORE_TOOL: "PreToolUse",
272
- CommandLifecycleHook.BEFORE_PROMPT: "UserPromptSubmit",
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 commands stored in .gemini/commands/.
334
- Commands use colon (:) for namespacing instead of dot (.).
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 command-level hooks. Hooks are configured
337
- globally in settings.json, not per-command. Therefore, hook_name_mapping
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
- command_template = "command-job-step.toml.jinja"
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 commands, or restart your Gemini CLI session."
369
+ "Run '/memory refresh' to reload skills, or restart your Gemini CLI session."
351
370
  )
352
371
 
353
- # Gemini CLI does NOT support command-level hooks
354
- # Hooks are global/project-level in settings.json, not per-command
355
- hook_name_mapping: ClassVar[dict[CommandLifecycleHook, str]] = {}
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 get_command_filename(self, job_name: str, step_id: str) -> str:
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 command.
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
- Command filename path (e.g., "my_job/step_one.toml")
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 command-level hooks. All hooks are
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 command-level hooks)
423
+ 0 (Gemini does not support skill-level hooks)
387
424
  """
388
- # Gemini CLI does not support command-level hooks
389
- # Hooks are configured globally in settings.json, not per-command
425
+ # Gemini CLI does not support skill-level hooks
426
+ # Hooks are configured globally in settings.json, not per-skill
390
427
  return 0