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.
- deepwork/cli/install.py +121 -32
- 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 +511 -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 +514 -0
- deepwork/hooks/wrapper.py +363 -0
- deepwork/schemas/job_schema.py +14 -1
- deepwork/schemas/rules_schema.py +103 -0
- deepwork/standard_jobs/deepwork_jobs/AGENTS.md +60 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +41 -56
- deepwork/standard_jobs/deepwork_jobs/make_new_job.sh +134 -0
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +29 -63
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +62 -263
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +4 -62
- deepwork/standard_jobs/deepwork_jobs/templates/agents.md.template +32 -0
- deepwork/standard_jobs/deepwork_jobs/templates/job.yml.example +73 -0
- deepwork/standard_jobs/deepwork_jobs/templates/job.yml.template +56 -0
- deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.example +82 -0
- deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.template +58 -0
- deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml +8 -0
- deepwork/standard_jobs/deepwork_rules/job.yml +39 -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 +45 -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.1.1.dist-info → deepwork-0.3.0.dist-info}/METADATA +54 -24
- deepwork-0.3.0.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/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/gemini/command-job-step.toml.jinja +0 -169
- deepwork-0.1.1.dist-info/RECORD +0 -41
- /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/capture_prompt_work_tree.sh +0 -0
- /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/user_prompt_submit.sh +0 -0
- {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/WHEEL +0 -0
- {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/entry_points.txt +0 -0
- {deepwork-0.1.1.dist-info → deepwork-0.3.0.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Execute command actions for rules."""
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from deepwork.core.rules_parser import CommandAction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CommandResult:
|
|
13
|
+
"""Result of executing a command."""
|
|
14
|
+
|
|
15
|
+
success: bool
|
|
16
|
+
exit_code: int
|
|
17
|
+
stdout: str
|
|
18
|
+
stderr: str
|
|
19
|
+
command: str # The actual command that was run
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def substitute_command_variables(
|
|
23
|
+
command_template: str,
|
|
24
|
+
file: str | None = None,
|
|
25
|
+
files: list[str] | None = None,
|
|
26
|
+
repo_root: Path | None = None,
|
|
27
|
+
) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Substitute template variables in a command string.
|
|
30
|
+
|
|
31
|
+
Variables:
|
|
32
|
+
- {file} - Single file path
|
|
33
|
+
- {files} - Space-separated file paths
|
|
34
|
+
- {repo_root} - Repository root directory
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
command_template: Command string with {var} placeholders
|
|
38
|
+
file: Single file path (for run_for: each_match)
|
|
39
|
+
files: List of file paths (for run_for: all_matches)
|
|
40
|
+
repo_root: Repository root path
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Command string with variables substituted
|
|
44
|
+
"""
|
|
45
|
+
result = command_template
|
|
46
|
+
|
|
47
|
+
if file is not None:
|
|
48
|
+
# Quote file path to prevent command injection
|
|
49
|
+
result = result.replace("{file}", shlex.quote(file))
|
|
50
|
+
|
|
51
|
+
if files is not None:
|
|
52
|
+
# Quote each file path individually
|
|
53
|
+
quoted_files = " ".join(shlex.quote(f) for f in files)
|
|
54
|
+
result = result.replace("{files}", quoted_files)
|
|
55
|
+
|
|
56
|
+
if repo_root is not None:
|
|
57
|
+
result = result.replace("{repo_root}", shlex.quote(str(repo_root)))
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def execute_command(
|
|
63
|
+
command: str,
|
|
64
|
+
cwd: Path | None = None,
|
|
65
|
+
timeout: int = 60,
|
|
66
|
+
) -> CommandResult:
|
|
67
|
+
"""
|
|
68
|
+
Execute a command and capture output.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
command: Command string to execute
|
|
72
|
+
cwd: Working directory (defaults to current directory)
|
|
73
|
+
timeout: Timeout in seconds
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
CommandResult with execution details
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
# Run command as shell to support pipes, etc.
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
command,
|
|
82
|
+
shell=True,
|
|
83
|
+
cwd=cwd,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return CommandResult(
|
|
90
|
+
success=result.returncode == 0,
|
|
91
|
+
exit_code=result.returncode,
|
|
92
|
+
stdout=result.stdout,
|
|
93
|
+
stderr=result.stderr,
|
|
94
|
+
command=command,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except subprocess.TimeoutExpired:
|
|
98
|
+
return CommandResult(
|
|
99
|
+
success=False,
|
|
100
|
+
exit_code=-1,
|
|
101
|
+
stdout="",
|
|
102
|
+
stderr=f"Command timed out after {timeout} seconds",
|
|
103
|
+
command=command,
|
|
104
|
+
)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return CommandResult(
|
|
107
|
+
success=False,
|
|
108
|
+
exit_code=-1,
|
|
109
|
+
stdout="",
|
|
110
|
+
stderr=str(e),
|
|
111
|
+
command=command,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run_command_action(
|
|
116
|
+
action: CommandAction,
|
|
117
|
+
trigger_files: list[str],
|
|
118
|
+
repo_root: Path | None = None,
|
|
119
|
+
) -> list[CommandResult]:
|
|
120
|
+
"""
|
|
121
|
+
Run a command action for the given trigger files.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
action: CommandAction configuration
|
|
125
|
+
trigger_files: Files that triggered the rule
|
|
126
|
+
repo_root: Repository root path
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of CommandResult (one per command execution)
|
|
130
|
+
"""
|
|
131
|
+
results: list[CommandResult] = []
|
|
132
|
+
|
|
133
|
+
if action.run_for == "each_match":
|
|
134
|
+
# Run command for each file individually
|
|
135
|
+
for file_path in trigger_files:
|
|
136
|
+
command = substitute_command_variables(
|
|
137
|
+
action.command,
|
|
138
|
+
file=file_path,
|
|
139
|
+
repo_root=repo_root,
|
|
140
|
+
)
|
|
141
|
+
result = execute_command(command, cwd=repo_root)
|
|
142
|
+
results.append(result)
|
|
143
|
+
|
|
144
|
+
elif action.run_for == "all_matches":
|
|
145
|
+
# Run command once with all files
|
|
146
|
+
command = substitute_command_variables(
|
|
147
|
+
action.command,
|
|
148
|
+
files=trigger_files,
|
|
149
|
+
repo_root=repo_root,
|
|
150
|
+
)
|
|
151
|
+
result = execute_command(command, cwd=repo_root)
|
|
152
|
+
results.append(result)
|
|
153
|
+
|
|
154
|
+
return results
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def all_commands_succeeded(results: list[CommandResult]) -> bool:
|
|
158
|
+
"""Check if all command executions succeeded."""
|
|
159
|
+
return all(r.success for r in results)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def format_command_errors(results: list[CommandResult]) -> str:
|
|
163
|
+
"""Format error messages from failed commands."""
|
|
164
|
+
errors: list[str] = []
|
|
165
|
+
for result in results:
|
|
166
|
+
if not result.success:
|
|
167
|
+
msg = f"Command failed: {result.command}\n"
|
|
168
|
+
if result.stderr:
|
|
169
|
+
msg += f"Error: {result.stderr}\n"
|
|
170
|
+
if result.exit_code != 0:
|
|
171
|
+
msg += f"Exit code: {result.exit_code}\n"
|
|
172
|
+
errors.append(msg)
|
|
173
|
+
return "\n".join(errors)
|
deepwork/core/generator.py
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Skill file generator using Jinja2 templates."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
|
7
7
|
|
|
8
|
-
from deepwork.core.adapters import AgentAdapter,
|
|
8
|
+
from deepwork.core.adapters import AgentAdapter, SkillLifecycleHook
|
|
9
9
|
from deepwork.core.parser import JobDefinition, Step
|
|
10
10
|
from deepwork.schemas.job_schema import LIFECYCLE_HOOK_EVENTS
|
|
11
11
|
from deepwork.utils.fs import safe_read, safe_write
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class GeneratorError(Exception):
|
|
15
|
-
"""Exception raised for
|
|
15
|
+
"""Exception raised for skill generation errors."""
|
|
16
16
|
|
|
17
17
|
pass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class
|
|
21
|
-
"""Generates
|
|
20
|
+
class SkillGenerator:
|
|
21
|
+
"""Generates skill files from job definitions."""
|
|
22
22
|
|
|
23
23
|
def __init__(self, templates_dir: Path | str | None = None):
|
|
24
24
|
"""
|
|
@@ -163,7 +163,7 @@ class CommandGenerator:
|
|
|
163
163
|
for event in LIFECYCLE_HOOK_EVENTS:
|
|
164
164
|
if event in step.hooks:
|
|
165
165
|
# Get platform-specific event name from adapter
|
|
166
|
-
hook_enum =
|
|
166
|
+
hook_enum = SkillLifecycleHook(event)
|
|
167
167
|
platform_event_name = adapter.get_platform_hook_name(hook_enum)
|
|
168
168
|
if platform_event_name:
|
|
169
169
|
hook_contexts = [
|
|
@@ -175,7 +175,7 @@ class CommandGenerator:
|
|
|
175
175
|
|
|
176
176
|
# Backward compatibility: stop_hooks is after_agent hooks
|
|
177
177
|
stop_hooks = hooks.get(
|
|
178
|
-
adapter.get_platform_hook_name(
|
|
178
|
+
adapter.get_platform_hook_name(SkillLifecycleHook.AFTER_AGENT) or "Stop", []
|
|
179
179
|
)
|
|
180
180
|
|
|
181
181
|
return {
|
|
@@ -199,9 +199,117 @@ class CommandGenerator:
|
|
|
199
199
|
"is_standalone": is_standalone,
|
|
200
200
|
"hooks": hooks, # New: all hooks by platform event name
|
|
201
201
|
"stop_hooks": stop_hooks, # Backward compat: after_agent hooks only
|
|
202
|
+
"quality_criteria": step.quality_criteria, # Declarative criteria with framing
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
def
|
|
205
|
+
def _build_meta_skill_context(
|
|
206
|
+
self, job: JobDefinition, adapter: AgentAdapter
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
"""
|
|
209
|
+
Build template context for a job's meta-skill.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
job: Job definition
|
|
213
|
+
adapter: Agent adapter for platform-specific configuration
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Template context dictionary
|
|
217
|
+
"""
|
|
218
|
+
# Build step info for the meta-skill
|
|
219
|
+
steps_info = []
|
|
220
|
+
for step in job.steps:
|
|
221
|
+
skill_filename = adapter.get_step_skill_filename(job.name, step.id, step.exposed)
|
|
222
|
+
# Extract just the skill name (without path and extension)
|
|
223
|
+
# For Claude: job_name.step_id/SKILL.md -> job_name.step_id
|
|
224
|
+
# For Gemini: job_name/step_id.toml -> job_name:step_id
|
|
225
|
+
if adapter.name == "gemini":
|
|
226
|
+
# Gemini uses colon for namespacing: job_name:step_id
|
|
227
|
+
parts = skill_filename.replace(".toml", "").split("/")
|
|
228
|
+
skill_name = ":".join(parts)
|
|
229
|
+
else:
|
|
230
|
+
# Claude uses directory/SKILL.md format, extract directory name
|
|
231
|
+
# job_name.step_id/SKILL.md -> job_name.step_id
|
|
232
|
+
skill_name = skill_filename.replace("/SKILL.md", "")
|
|
233
|
+
|
|
234
|
+
steps_info.append(
|
|
235
|
+
{
|
|
236
|
+
"id": step.id,
|
|
237
|
+
"name": step.name,
|
|
238
|
+
"description": step.description,
|
|
239
|
+
"command_name": skill_name,
|
|
240
|
+
"dependencies": step.dependencies,
|
|
241
|
+
"exposed": step.exposed,
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"job_name": job.name,
|
|
247
|
+
"job_version": job.version,
|
|
248
|
+
"job_summary": job.summary,
|
|
249
|
+
"job_description": job.description,
|
|
250
|
+
"total_steps": len(job.steps),
|
|
251
|
+
"steps": steps_info,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
def generate_meta_skill(
|
|
255
|
+
self,
|
|
256
|
+
job: JobDefinition,
|
|
257
|
+
adapter: AgentAdapter,
|
|
258
|
+
output_dir: Path | str,
|
|
259
|
+
) -> Path:
|
|
260
|
+
"""
|
|
261
|
+
Generate the meta-skill file for a job.
|
|
262
|
+
|
|
263
|
+
The meta-skill is the primary user interface for a job, routing
|
|
264
|
+
user intent to the appropriate step.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
job: Job definition
|
|
268
|
+
adapter: Agent adapter for the target platform
|
|
269
|
+
output_dir: Directory to write skill file to
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Path to generated meta-skill file
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
GeneratorError: If generation fails
|
|
276
|
+
"""
|
|
277
|
+
output_dir = Path(output_dir)
|
|
278
|
+
|
|
279
|
+
# Create skills subdirectory if needed
|
|
280
|
+
skills_dir = output_dir / adapter.skills_dir
|
|
281
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
|
|
283
|
+
# Build context
|
|
284
|
+
context = self._build_meta_skill_context(job, adapter)
|
|
285
|
+
|
|
286
|
+
# Load and render template
|
|
287
|
+
env = self._get_jinja_env(adapter)
|
|
288
|
+
try:
|
|
289
|
+
template = env.get_template(adapter.meta_skill_template)
|
|
290
|
+
except TemplateNotFound as e:
|
|
291
|
+
raise GeneratorError(f"Meta-skill template not found: {e}") from e
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
rendered = template.render(**context)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
raise GeneratorError(f"Meta-skill template rendering failed: {e}") from e
|
|
297
|
+
|
|
298
|
+
# Write meta-skill file
|
|
299
|
+
skill_filename = adapter.get_meta_skill_filename(job.name)
|
|
300
|
+
skill_path = skills_dir / skill_filename
|
|
301
|
+
|
|
302
|
+
# Ensure parent directories exist (for Gemini's job_name/index.toml structure)
|
|
303
|
+
skill_path.parent.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
safe_write(skill_path, rendered)
|
|
307
|
+
except Exception as e:
|
|
308
|
+
raise GeneratorError(f"Failed to write meta-skill file: {e}") from e
|
|
309
|
+
|
|
310
|
+
return skill_path
|
|
311
|
+
|
|
312
|
+
def generate_step_skill(
|
|
205
313
|
self,
|
|
206
314
|
job: JobDefinition,
|
|
207
315
|
step: Step,
|
|
@@ -209,25 +317,25 @@ class CommandGenerator:
|
|
|
209
317
|
output_dir: Path | str,
|
|
210
318
|
) -> Path:
|
|
211
319
|
"""
|
|
212
|
-
Generate
|
|
320
|
+
Generate skill file for a single step.
|
|
213
321
|
|
|
214
322
|
Args:
|
|
215
323
|
job: Job definition
|
|
216
|
-
step: Step to generate
|
|
324
|
+
step: Step to generate skill for
|
|
217
325
|
adapter: Agent adapter for the target platform
|
|
218
|
-
output_dir: Directory to write
|
|
326
|
+
output_dir: Directory to write skill file to
|
|
219
327
|
|
|
220
328
|
Returns:
|
|
221
|
-
Path to generated
|
|
329
|
+
Path to generated skill file
|
|
222
330
|
|
|
223
331
|
Raises:
|
|
224
332
|
GeneratorError: If generation fails
|
|
225
333
|
"""
|
|
226
334
|
output_dir = Path(output_dir)
|
|
227
335
|
|
|
228
|
-
# Create
|
|
229
|
-
|
|
230
|
-
|
|
336
|
+
# Create skills subdirectory if needed
|
|
337
|
+
skills_dir = output_dir / adapter.skills_dir
|
|
338
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
231
339
|
|
|
232
340
|
# Find step index
|
|
233
341
|
try:
|
|
@@ -235,13 +343,14 @@ class CommandGenerator:
|
|
|
235
343
|
except StopIteration as e:
|
|
236
344
|
raise GeneratorError(f"Step '{step.id}' not found in job '{job.name}'") from e
|
|
237
345
|
|
|
238
|
-
# Build context
|
|
346
|
+
# Build context (include exposed for template user-invocable setting)
|
|
239
347
|
context = self._build_step_context(job, step, step_index, adapter)
|
|
348
|
+
context["exposed"] = step.exposed
|
|
240
349
|
|
|
241
350
|
# Load and render template
|
|
242
351
|
env = self._get_jinja_env(adapter)
|
|
243
352
|
try:
|
|
244
|
-
template = env.get_template(adapter.
|
|
353
|
+
template = env.get_template(adapter.skill_template)
|
|
245
354
|
except TemplateNotFound as e:
|
|
246
355
|
raise GeneratorError(f"Template not found: {e}") from e
|
|
247
356
|
|
|
@@ -250,41 +359,49 @@ class CommandGenerator:
|
|
|
250
359
|
except Exception as e:
|
|
251
360
|
raise GeneratorError(f"Template rendering failed: {e}") from e
|
|
252
361
|
|
|
253
|
-
# Write
|
|
254
|
-
|
|
255
|
-
|
|
362
|
+
# Write skill file
|
|
363
|
+
skill_filename = adapter.get_step_skill_filename(job.name, step.id, step.exposed)
|
|
364
|
+
skill_path = skills_dir / skill_filename
|
|
365
|
+
|
|
366
|
+
# Ensure parent directories exist (for Gemini's job_name/step_id.toml structure)
|
|
367
|
+
skill_path.parent.mkdir(parents=True, exist_ok=True)
|
|
256
368
|
|
|
257
369
|
try:
|
|
258
|
-
safe_write(
|
|
370
|
+
safe_write(skill_path, rendered)
|
|
259
371
|
except Exception as e:
|
|
260
|
-
raise GeneratorError(f"Failed to write
|
|
372
|
+
raise GeneratorError(f"Failed to write skill file: {e}") from e
|
|
261
373
|
|
|
262
|
-
return
|
|
374
|
+
return skill_path
|
|
263
375
|
|
|
264
|
-
def
|
|
376
|
+
def generate_all_skills(
|
|
265
377
|
self,
|
|
266
378
|
job: JobDefinition,
|
|
267
379
|
adapter: AgentAdapter,
|
|
268
380
|
output_dir: Path | str,
|
|
269
381
|
) -> list[Path]:
|
|
270
382
|
"""
|
|
271
|
-
Generate
|
|
383
|
+
Generate all skill files for a job: meta-skill and step skills.
|
|
272
384
|
|
|
273
385
|
Args:
|
|
274
386
|
job: Job definition
|
|
275
387
|
adapter: Agent adapter for the target platform
|
|
276
|
-
output_dir: Directory to write
|
|
388
|
+
output_dir: Directory to write skill files to
|
|
277
389
|
|
|
278
390
|
Returns:
|
|
279
|
-
List of paths to generated
|
|
391
|
+
List of paths to generated skill files (meta-skill first, then steps)
|
|
280
392
|
|
|
281
393
|
Raises:
|
|
282
394
|
GeneratorError: If generation fails
|
|
283
395
|
"""
|
|
284
|
-
|
|
396
|
+
skill_paths = []
|
|
397
|
+
|
|
398
|
+
# Generate meta-skill first (job-level entry point)
|
|
399
|
+
meta_skill_path = self.generate_meta_skill(job, adapter, output_dir)
|
|
400
|
+
skill_paths.append(meta_skill_path)
|
|
285
401
|
|
|
402
|
+
# Generate step skills
|
|
286
403
|
for step in job.steps:
|
|
287
|
-
|
|
288
|
-
|
|
404
|
+
skill_path = self.generate_step_skill(job, step, adapter, output_dir)
|
|
405
|
+
skill_paths.append(skill_path)
|
|
289
406
|
|
|
290
|
-
return
|
|
407
|
+
return skill_paths
|
deepwork/core/hooks_syncer.py
CHANGED
|
@@ -19,27 +19,42 @@ class HooksSyncError(Exception):
|
|
|
19
19
|
class HookEntry:
|
|
20
20
|
"""Represents a single hook entry for a lifecycle event."""
|
|
21
21
|
|
|
22
|
-
script: str # Script filename
|
|
23
22
|
job_name: str # Job that provides this hook
|
|
24
23
|
job_dir: Path # Full path to job directory
|
|
24
|
+
script: str | None = None # Script filename (if script-based hook)
|
|
25
|
+
module: str | None = None # Python module (if module-based hook)
|
|
25
26
|
|
|
26
|
-
def
|
|
27
|
+
def get_command(self, project_path: Path) -> str:
|
|
27
28
|
"""
|
|
28
|
-
Get the
|
|
29
|
+
Get the command to run this hook.
|
|
29
30
|
|
|
30
31
|
Args:
|
|
31
32
|
project_path: Path to project root
|
|
32
33
|
|
|
33
34
|
Returns:
|
|
34
|
-
|
|
35
|
+
Command string to execute
|
|
35
36
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
if self.module:
|
|
38
|
+
# Python module - run directly with python -m
|
|
39
|
+
return f"python -m {self.module}"
|
|
40
|
+
elif self.script:
|
|
41
|
+
# Script path is: .deepwork/jobs/{job_name}/hooks/{script}
|
|
42
|
+
script_path = self.job_dir / "hooks" / self.script
|
|
43
|
+
try:
|
|
44
|
+
return str(script_path.relative_to(project_path))
|
|
45
|
+
except ValueError:
|
|
46
|
+
# If not relative, return the full path
|
|
47
|
+
return str(script_path)
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError("HookEntry must have either script or module")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class HookSpec:
|
|
54
|
+
"""Specification for a single hook (either script or module)."""
|
|
55
|
+
|
|
56
|
+
script: str | None = None
|
|
57
|
+
module: str | None = None
|
|
43
58
|
|
|
44
59
|
|
|
45
60
|
@dataclass
|
|
@@ -48,7 +63,7 @@ class JobHooks:
|
|
|
48
63
|
|
|
49
64
|
job_name: str
|
|
50
65
|
job_dir: Path
|
|
51
|
-
hooks: dict[str, list[
|
|
66
|
+
hooks: dict[str, list[HookSpec]] = field(default_factory=dict) # event -> [HookSpec]
|
|
52
67
|
|
|
53
68
|
@classmethod
|
|
54
69
|
def from_job_dir(cls, job_dir: Path) -> "JobHooks | None":
|
|
@@ -74,13 +89,23 @@ class JobHooks:
|
|
|
74
89
|
if not data or not isinstance(data, dict):
|
|
75
90
|
return None
|
|
76
91
|
|
|
77
|
-
# Parse hooks - each key is an event, value is list of scripts
|
|
78
|
-
hooks: dict[str, list[
|
|
79
|
-
for event,
|
|
80
|
-
if isinstance(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
# Parse hooks - each key is an event, value is list of scripts or module specs
|
|
93
|
+
hooks: dict[str, list[HookSpec]] = {}
|
|
94
|
+
for event, entries in data.items():
|
|
95
|
+
if not isinstance(entries, list):
|
|
96
|
+
entries = [entries]
|
|
97
|
+
|
|
98
|
+
hook_specs: list[HookSpec] = []
|
|
99
|
+
for entry in entries:
|
|
100
|
+
if isinstance(entry, str):
|
|
101
|
+
# Simple script filename
|
|
102
|
+
hook_specs.append(HookSpec(script=entry))
|
|
103
|
+
elif isinstance(entry, dict) and "module" in entry:
|
|
104
|
+
# Python module specification
|
|
105
|
+
hook_specs.append(HookSpec(module=entry["module"]))
|
|
106
|
+
|
|
107
|
+
if hook_specs:
|
|
108
|
+
hooks[event] = hook_specs
|
|
84
109
|
|
|
85
110
|
if not hooks:
|
|
86
111
|
return None
|
|
@@ -134,17 +159,18 @@ def merge_hooks_for_platform(
|
|
|
134
159
|
merged: dict[str, list[dict[str, Any]]] = {}
|
|
135
160
|
|
|
136
161
|
for job_hooks in job_hooks_list:
|
|
137
|
-
for event,
|
|
162
|
+
for event, hook_specs in job_hooks.hooks.items():
|
|
138
163
|
if event not in merged:
|
|
139
164
|
merged[event] = []
|
|
140
165
|
|
|
141
|
-
for
|
|
166
|
+
for spec in hook_specs:
|
|
142
167
|
entry = HookEntry(
|
|
143
|
-
script=script,
|
|
144
168
|
job_name=job_hooks.job_name,
|
|
145
169
|
job_dir=job_hooks.job_dir,
|
|
170
|
+
script=spec.script,
|
|
171
|
+
module=spec.module,
|
|
146
172
|
)
|
|
147
|
-
|
|
173
|
+
command = entry.get_command(project_path)
|
|
148
174
|
|
|
149
175
|
# Create hook configuration for Claude Code format
|
|
150
176
|
hook_config = {
|
|
@@ -152,13 +178,13 @@ def merge_hooks_for_platform(
|
|
|
152
178
|
"hooks": [
|
|
153
179
|
{
|
|
154
180
|
"type": "command",
|
|
155
|
-
"command":
|
|
181
|
+
"command": command,
|
|
156
182
|
}
|
|
157
183
|
],
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
# Check if this hook is already present (avoid duplicates)
|
|
161
|
-
if not _hook_already_present(merged[event],
|
|
187
|
+
if not _hook_already_present(merged[event], command):
|
|
162
188
|
merged[event].append(hook_config)
|
|
163
189
|
|
|
164
190
|
return merged
|
deepwork/core/parser.py
CHANGED
|
@@ -108,6 +108,12 @@ class Step:
|
|
|
108
108
|
# Event names: after_agent, before_tool, before_prompt
|
|
109
109
|
hooks: dict[str, list[HookAction]] = field(default_factory=dict)
|
|
110
110
|
|
|
111
|
+
# If true, skill is user-invocable in menus. Default: false (hidden from menus).
|
|
112
|
+
exposed: bool = False
|
|
113
|
+
|
|
114
|
+
# Declarative quality criteria rendered with standard evaluation framing
|
|
115
|
+
quality_criteria: list[str] = field(default_factory=list)
|
|
116
|
+
|
|
111
117
|
@property
|
|
112
118
|
def stop_hooks(self) -> list[HookAction]:
|
|
113
119
|
"""
|
|
@@ -144,6 +150,8 @@ class Step:
|
|
|
144
150
|
outputs=data["outputs"],
|
|
145
151
|
dependencies=data.get("dependencies", []),
|
|
146
152
|
hooks=hooks,
|
|
153
|
+
exposed=data.get("exposed", False),
|
|
154
|
+
quality_criteria=data.get("quality_criteria", []),
|
|
147
155
|
)
|
|
148
156
|
|
|
149
157
|
|