deepwork 0.5.1__py3-none-any.whl → 0.7.0a1__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 (66) hide show
  1. deepwork/__init__.py +1 -1
  2. deepwork/cli/hook.py +3 -4
  3. deepwork/cli/install.py +70 -117
  4. deepwork/cli/main.py +2 -2
  5. deepwork/cli/serve.py +133 -0
  6. deepwork/cli/sync.py +93 -58
  7. deepwork/core/adapters.py +91 -102
  8. deepwork/core/generator.py +19 -386
  9. deepwork/core/hooks_syncer.py +1 -1
  10. deepwork/core/parser.py +270 -1
  11. deepwork/hooks/README.md +0 -44
  12. deepwork/hooks/__init__.py +3 -6
  13. deepwork/hooks/check_version.sh +54 -21
  14. deepwork/mcp/__init__.py +23 -0
  15. deepwork/mcp/quality_gate.py +347 -0
  16. deepwork/mcp/schemas.py +263 -0
  17. deepwork/mcp/server.py +253 -0
  18. deepwork/mcp/state.py +422 -0
  19. deepwork/mcp/tools.py +394 -0
  20. deepwork/schemas/job.schema.json +347 -0
  21. deepwork/schemas/job_schema.py +27 -239
  22. deepwork/standard_jobs/deepwork_jobs/doc_specs/job_spec.md +9 -15
  23. deepwork/standard_jobs/deepwork_jobs/job.yml +146 -46
  24. deepwork/standard_jobs/deepwork_jobs/steps/define.md +100 -33
  25. deepwork/standard_jobs/deepwork_jobs/steps/errata.md +154 -0
  26. deepwork/standard_jobs/deepwork_jobs/steps/fix_jobs.md +207 -0
  27. deepwork/standard_jobs/deepwork_jobs/steps/fix_settings.md +177 -0
  28. deepwork/standard_jobs/deepwork_jobs/steps/implement.md +22 -138
  29. deepwork/standard_jobs/deepwork_jobs/steps/iterate.md +221 -0
  30. deepwork/standard_jobs/deepwork_jobs/steps/learn.md +2 -26
  31. deepwork/standard_jobs/deepwork_jobs/steps/test.md +154 -0
  32. deepwork/standard_jobs/deepwork_jobs/templates/job.yml.template +2 -0
  33. deepwork/templates/claude/settings.json +16 -0
  34. deepwork/templates/claude/skill-deepwork.md.jinja +37 -0
  35. deepwork/templates/gemini/skill-deepwork.md.jinja +37 -0
  36. deepwork-0.7.0a1.dist-info/METADATA +317 -0
  37. deepwork-0.7.0a1.dist-info/RECORD +64 -0
  38. deepwork/cli/rules.py +0 -32
  39. deepwork/core/command_executor.py +0 -190
  40. deepwork/core/pattern_matcher.py +0 -271
  41. deepwork/core/rules_parser.py +0 -559
  42. deepwork/core/rules_queue.py +0 -321
  43. deepwork/hooks/rules_check.py +0 -759
  44. deepwork/schemas/rules_schema.py +0 -135
  45. deepwork/standard_jobs/deepwork_jobs/steps/review_job_spec.md +0 -208
  46. deepwork/standard_jobs/deepwork_jobs/templates/doc_spec.md.example +0 -86
  47. deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +0 -38
  48. deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml +0 -8
  49. deepwork/standard_jobs/deepwork_rules/hooks/user_prompt_submit.sh +0 -16
  50. deepwork/standard_jobs/deepwork_rules/job.yml +0 -49
  51. deepwork/standard_jobs/deepwork_rules/rules/.gitkeep +0 -13
  52. deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example +0 -10
  53. deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example +0 -10
  54. deepwork/standard_jobs/deepwork_rules/rules/security-review.md.example +0 -11
  55. deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md +0 -46
  56. deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example +0 -13
  57. deepwork/standard_jobs/deepwork_rules/steps/define.md +0 -249
  58. deepwork/templates/claude/skill-job-meta.md.jinja +0 -77
  59. deepwork/templates/claude/skill-job-step.md.jinja +0 -235
  60. deepwork/templates/gemini/skill-job-meta.toml.jinja +0 -76
  61. deepwork/templates/gemini/skill-job-step.toml.jinja +0 -162
  62. deepwork-0.5.1.dist-info/METADATA +0 -381
  63. deepwork-0.5.1.dist-info/RECORD +0 -72
  64. {deepwork-0.5.1.dist-info → deepwork-0.7.0a1.dist-info}/WHEEL +0 -0
  65. {deepwork-0.5.1.dist-info → deepwork-0.7.0a1.dist-info}/entry_points.txt +0 -0
  66. {deepwork-0.5.1.dist-info → deepwork-0.7.0a1.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,19 +1,11 @@
1
1
  """Skill file generator using Jinja2 templates."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any
5
4
 
6
5
  from jinja2 import Environment, FileSystemLoader, TemplateNotFound
7
6
 
8
- from deepwork.core.adapters import AgentAdapter, SkillLifecycleHook
9
- from deepwork.core.doc_spec_parser import (
10
- DocSpec,
11
- DocSpecParseError,
12
- parse_doc_spec_file,
13
- )
14
- from deepwork.core.parser import JobDefinition, Step
15
- from deepwork.schemas.job_schema import LIFECYCLE_HOOK_EVENTS
16
- from deepwork.utils.fs import safe_read, safe_write
7
+ from deepwork.core.adapters import AgentAdapter
8
+ from deepwork.utils.fs import safe_write
17
9
 
18
10
 
19
11
  class GeneratorError(Exception):
@@ -42,35 +34,6 @@ class SkillGenerator:
42
34
  if not self.templates_dir.exists():
43
35
  raise GeneratorError(f"Templates directory not found: {self.templates_dir}")
44
36
 
45
- # Cache for loaded doc specs (keyed by absolute file path)
46
- self._doc_spec_cache: dict[Path, DocSpec] = {}
47
-
48
- def _load_doc_spec(self, project_root: Path, doc_spec_path: str) -> DocSpec | None:
49
- """
50
- Load a doc spec by file path with caching.
51
-
52
- Args:
53
- project_root: Path to project root
54
- doc_spec_path: Relative path to doc spec file (e.g., ".deepwork/doc_specs/report.md")
55
-
56
- Returns:
57
- DocSpec if file exists and parses, None otherwise
58
- """
59
- full_path = project_root / doc_spec_path
60
- if full_path in self._doc_spec_cache:
61
- return self._doc_spec_cache[full_path]
62
-
63
- if not full_path.exists():
64
- return None
65
-
66
- try:
67
- doc_spec = parse_doc_spec_file(full_path)
68
- except DocSpecParseError:
69
- return None
70
-
71
- self._doc_spec_cache[full_path] = doc_spec
72
- return doc_spec
73
-
74
37
  def _get_jinja_env(self, adapter: AgentAdapter) -> Environment:
75
38
  """
76
39
  Get Jinja2 environment for an adapter.
@@ -93,308 +56,20 @@ class SkillGenerator:
93
56
  lstrip_blocks=True,
94
57
  )
95
58
 
96
- def _is_standalone_step(self, job: JobDefinition, step: Step) -> bool:
97
- """
98
- Check if a step is standalone (disconnected from the main workflow).
99
-
100
- A standalone step has no dependencies AND no other steps depend on it.
101
-
102
- Args:
103
- job: Job definition
104
- step: Step to check
105
-
106
- Returns:
107
- True if step is standalone
108
- """
109
- # Step has dependencies - not standalone
110
- if step.dependencies:
111
- return False
112
-
113
- # Check if any other step depends on this step
114
- for other_step in job.steps:
115
- if step.id in other_step.dependencies:
116
- return False
117
-
118
- return True
119
-
120
- def _build_hook_context(self, job: JobDefinition, hook_action: Any) -> dict[str, Any]:
121
- """
122
- Build context for a single hook action.
123
-
124
- Args:
125
- job: Job definition
126
- hook_action: HookAction instance
127
-
128
- Returns:
129
- Hook context dictionary
130
- """
131
- hook_ctx: dict[str, Any] = {}
132
- if hook_action.is_prompt():
133
- hook_ctx["type"] = "prompt"
134
- hook_ctx["content"] = hook_action.prompt
135
- elif hook_action.is_prompt_file():
136
- hook_ctx["type"] = "prompt_file"
137
- hook_ctx["path"] = hook_action.prompt_file
138
- # Read the prompt file content
139
- prompt_file_path = job.job_dir / hook_action.prompt_file
140
- prompt_content = safe_read(prompt_file_path)
141
- if prompt_content is None:
142
- raise GeneratorError(f"Hook prompt file not found: {prompt_file_path}")
143
- hook_ctx["content"] = prompt_content
144
- elif hook_action.is_script():
145
- hook_ctx["type"] = "script"
146
- hook_ctx["path"] = hook_action.script
147
- return hook_ctx
148
-
149
- def _build_step_context(
150
- self,
151
- job: JobDefinition,
152
- step: Step,
153
- step_index: int,
154
- adapter: AgentAdapter,
155
- project_root: Path | None = None,
156
- ) -> dict[str, Any]:
157
- """
158
- Build template context for a step.
159
-
160
- Args:
161
- job: Job definition
162
- step: Step to generate context for
163
- step_index: Index of step in job (0-based)
164
- adapter: Agent adapter for platform-specific hook name mapping
165
- project_root: Optional project root for loading doc specs
166
-
167
- Returns:
168
- Template context dictionary
169
- """
170
- # Read step instructions
171
- instructions_file = job.job_dir / step.instructions_file
172
- instructions_content = safe_read(instructions_file)
173
- if instructions_content is None:
174
- raise GeneratorError(f"Step instructions file not found: {instructions_file}")
175
-
176
- # Separate user inputs and file inputs
177
- user_inputs = [
178
- {"name": inp.name, "description": inp.description}
179
- for inp in step.inputs
180
- if inp.is_user_input()
181
- ]
182
- file_inputs = [
183
- {"file": inp.file, "from_step": inp.from_step}
184
- for inp in step.inputs
185
- if inp.is_file_input()
186
- ]
187
-
188
- # Check if this is a standalone step
189
- is_standalone = self._is_standalone_step(job, step)
190
-
191
- # Determine next and previous steps (only for non-standalone steps)
192
- next_step = None
193
- prev_step = None
194
- if not is_standalone:
195
- if step_index < len(job.steps) - 1:
196
- next_step = job.steps[step_index + 1].id
197
- if step_index > 0:
198
- prev_step = job.steps[step_index - 1].id
199
-
200
- # Build hooks context for all lifecycle events
201
- # Structure: {platform_event_name: [hook_contexts]}
202
- hooks: dict[str, list[dict[str, Any]]] = {}
203
- for event in LIFECYCLE_HOOK_EVENTS:
204
- if event in step.hooks:
205
- # Get platform-specific event name from adapter
206
- hook_enum = SkillLifecycleHook(event)
207
- platform_event_name = adapter.get_platform_hook_name(hook_enum)
208
- if platform_event_name:
209
- hook_contexts = [
210
- self._build_hook_context(job, hook_action)
211
- for hook_action in step.hooks[event]
212
- ]
213
- if hook_contexts:
214
- hooks[platform_event_name] = hook_contexts
215
-
216
- # Claude Code has separate Stop and SubagentStop events. When a Stop hook
217
- # is defined, also register it for SubagentStop so it triggers for both
218
- # the main agent and subagents.
219
- if "Stop" in hooks:
220
- hooks["SubagentStop"] = hooks["Stop"]
221
-
222
- # Backward compatibility: stop_hooks is after_agent hooks
223
- stop_hooks = hooks.get(
224
- adapter.get_platform_hook_name(SkillLifecycleHook.AFTER_AGENT) or "Stop", []
225
- )
226
-
227
- # Build rich outputs context with doc spec information
228
- outputs_context = []
229
- for output in step.outputs:
230
- output_ctx: dict[str, Any] = {
231
- "file": output.file,
232
- "has_doc_spec": output.has_doc_spec(),
233
- }
234
- if output.has_doc_spec() and output.doc_spec and project_root:
235
- doc_spec = self._load_doc_spec(project_root, output.doc_spec)
236
- if doc_spec:
237
- output_ctx["doc_spec"] = {
238
- "path": output.doc_spec,
239
- "name": doc_spec.name,
240
- "description": doc_spec.description,
241
- "target_audience": doc_spec.target_audience,
242
- "quality_criteria": [
243
- {"name": c.name, "description": c.description}
244
- for c in doc_spec.quality_criteria
245
- ],
246
- "example_document": doc_spec.example_document,
247
- }
248
- outputs_context.append(output_ctx)
249
-
250
- return {
251
- "job_name": job.name,
252
- "job_version": job.version,
253
- "job_summary": job.summary,
254
- "job_description": job.description,
255
- "step_id": step.id,
256
- "step_name": step.name,
257
- "step_description": step.description,
258
- "step_number": step_index + 1, # 1-based for display
259
- "total_steps": len(job.steps),
260
- "instructions_file": step.instructions_file,
261
- "instructions_content": instructions_content,
262
- "user_inputs": user_inputs,
263
- "file_inputs": file_inputs,
264
- "outputs": outputs_context,
265
- "dependencies": step.dependencies,
266
- "next_step": next_step,
267
- "prev_step": prev_step,
268
- "is_standalone": is_standalone,
269
- "hooks": hooks, # New: all hooks by platform event name
270
- "stop_hooks": stop_hooks, # Backward compat: after_agent hooks only
271
- "quality_criteria": step.quality_criteria, # Declarative criteria with framing
272
- }
273
-
274
- def _build_meta_skill_context(
275
- self, job: JobDefinition, adapter: AgentAdapter
276
- ) -> dict[str, Any]:
277
- """
278
- Build template context for a job's meta-skill.
279
-
280
- Args:
281
- job: Job definition
282
- adapter: Agent adapter for platform-specific configuration
283
-
284
- Returns:
285
- Template context dictionary
286
- """
287
- # Build step info for the meta-skill
288
- steps_info = []
289
- for step in job.steps:
290
- skill_filename = adapter.get_step_skill_filename(job.name, step.id, step.exposed)
291
- # Extract just the skill name (without path and extension)
292
- # For Claude: job_name.step_id/SKILL.md -> job_name.step_id
293
- # For Gemini: job_name/step_id.toml -> job_name:step_id
294
- if adapter.name == "gemini":
295
- # Gemini uses colon for namespacing: job_name:step_id
296
- parts = skill_filename.replace(".toml", "").split("/")
297
- skill_name = ":".join(parts)
298
- else:
299
- # Claude uses directory/SKILL.md format, extract directory name
300
- # job_name.step_id/SKILL.md -> job_name.step_id
301
- skill_name = skill_filename.replace("/SKILL.md", "")
302
-
303
- steps_info.append(
304
- {
305
- "id": step.id,
306
- "name": step.name,
307
- "description": step.description,
308
- "command_name": skill_name,
309
- "dependencies": step.dependencies,
310
- "exposed": step.exposed,
311
- }
312
- )
313
-
314
- return {
315
- "job_name": job.name,
316
- "job_version": job.version,
317
- "job_summary": job.summary,
318
- "job_description": job.description,
319
- "total_steps": len(job.steps),
320
- "steps": steps_info,
321
- }
322
-
323
- def generate_meta_skill(
59
+ def generate_deepwork_skill(
324
60
  self,
325
- job: JobDefinition,
326
61
  adapter: AgentAdapter,
327
62
  output_dir: Path | str,
328
63
  ) -> Path:
329
64
  """
330
- Generate the meta-skill file for a job.
331
-
332
- The meta-skill is the primary user interface for a job, routing
333
- user intent to the appropriate step.
334
-
335
- Args:
336
- job: Job definition
337
- adapter: Agent adapter for the target platform
338
- output_dir: Directory to write skill file to
339
-
340
- Returns:
341
- Path to generated meta-skill file
342
-
343
- Raises:
344
- GeneratorError: If generation fails
345
- """
346
- output_dir = Path(output_dir)
347
-
348
- # Create skills subdirectory if needed
349
- skills_dir = output_dir / adapter.skills_dir
350
- skills_dir.mkdir(parents=True, exist_ok=True)
351
-
352
- # Build context
353
- context = self._build_meta_skill_context(job, adapter)
354
-
355
- # Load and render template
356
- env = self._get_jinja_env(adapter)
357
- try:
358
- template = env.get_template(adapter.meta_skill_template)
359
- except TemplateNotFound as e:
360
- raise GeneratorError(f"Meta-skill template not found: {e}") from e
361
-
362
- try:
363
- rendered = template.render(**context)
364
- except Exception as e:
365
- raise GeneratorError(f"Meta-skill template rendering failed: {e}") from e
366
-
367
- # Write meta-skill file
368
- skill_filename = adapter.get_meta_skill_filename(job.name)
369
- skill_path = skills_dir / skill_filename
370
-
371
- # Ensure parent directories exist (for Gemini's job_name/index.toml structure)
372
- skill_path.parent.mkdir(parents=True, exist_ok=True)
373
-
374
- try:
375
- safe_write(skill_path, rendered)
376
- except Exception as e:
377
- raise GeneratorError(f"Failed to write meta-skill file: {e}") from e
65
+ Generate the global /deepwork skill that instructs agents to use MCP tools.
378
66
 
379
- return skill_path
380
-
381
- def generate_step_skill(
382
- self,
383
- job: JobDefinition,
384
- step: Step,
385
- adapter: AgentAdapter,
386
- output_dir: Path | str,
387
- project_root: Path | str | None = None,
388
- ) -> Path:
389
- """
390
- Generate skill file for a single step.
67
+ This is a single skill that provides the main entry point for DeepWork,
68
+ directing agents to use the MCP server's tools for workflow management.
391
69
 
392
70
  Args:
393
- job: Job definition
394
- step: Step to generate skill for
395
71
  adapter: Agent adapter for the target platform
396
72
  output_dir: Directory to write skill file to
397
- project_root: Optional project root for loading doc specs (defaults to output_dir)
398
73
 
399
74
  Returns:
400
75
  Path to generated skill file
@@ -403,80 +78,38 @@ class SkillGenerator:
403
78
  GeneratorError: If generation fails
404
79
  """
405
80
  output_dir = Path(output_dir)
406
- project_root_path = Path(project_root) if project_root else output_dir
407
81
 
408
82
  # Create skills subdirectory if needed
409
83
  skills_dir = output_dir / adapter.skills_dir
410
84
  skills_dir.mkdir(parents=True, exist_ok=True)
411
85
 
412
- # Find step index
413
- try:
414
- step_index = next(i for i, s in enumerate(job.steps) if s.id == step.id)
415
- except StopIteration as e:
416
- raise GeneratorError(f"Step '{step.id}' not found in job '{job.name}'") from e
417
-
418
- # Build context (include exposed for template user-invocable setting)
419
- context = self._build_step_context(job, step, step_index, adapter, project_root_path)
420
- context["exposed"] = step.exposed
421
-
422
86
  # Load and render template
423
87
  env = self._get_jinja_env(adapter)
88
+ template_name = "skill-deepwork.md.jinja"
89
+
424
90
  try:
425
- template = env.get_template(adapter.skill_template)
91
+ template = env.get_template(template_name)
426
92
  except TemplateNotFound as e:
427
- raise GeneratorError(f"Template not found: {e}") from e
93
+ raise GeneratorError(f"DeepWork skill template not found: {e}") from e
428
94
 
429
95
  try:
430
- rendered = template.render(**context)
96
+ rendered = template.render()
431
97
  except Exception as e:
432
- raise GeneratorError(f"Template rendering failed: {e}") from e
98
+ raise GeneratorError(f"DeepWork skill template rendering failed: {e}") from e
433
99
 
434
100
  # Write skill file
435
- skill_filename = adapter.get_step_skill_filename(job.name, step.id, step.exposed)
436
- skill_path = skills_dir / skill_filename
101
+ # Use the adapter's convention for naming
102
+ if adapter.name == "gemini":
103
+ skill_filename = "deepwork/index.toml"
104
+ else:
105
+ skill_filename = "deepwork/SKILL.md"
437
106
 
438
- # Ensure parent directories exist (for Gemini's job_name/step_id.toml structure)
107
+ skill_path = skills_dir / skill_filename
439
108
  skill_path.parent.mkdir(parents=True, exist_ok=True)
440
109
 
441
110
  try:
442
111
  safe_write(skill_path, rendered)
443
112
  except Exception as e:
444
- raise GeneratorError(f"Failed to write skill file: {e}") from e
113
+ raise GeneratorError(f"Failed to write DeepWork skill file: {e}") from e
445
114
 
446
115
  return skill_path
447
-
448
- def generate_all_skills(
449
- self,
450
- job: JobDefinition,
451
- adapter: AgentAdapter,
452
- output_dir: Path | str,
453
- project_root: Path | str | None = None,
454
- ) -> list[Path]:
455
- """
456
- Generate all skill files for a job: meta-skill and step skills.
457
-
458
- Args:
459
- job: Job definition
460
- adapter: Agent adapter for the target platform
461
- output_dir: Directory to write skill files to
462
- project_root: Optional project root for loading doc specs (defaults to output_dir)
463
-
464
- Returns:
465
- List of paths to generated skill files (meta-skill first, then steps)
466
-
467
- Raises:
468
- GeneratorError: If generation fails
469
- """
470
- skill_paths = []
471
- project_root_path = Path(project_root) if project_root else Path(output_dir)
472
-
473
- # Generate meta-skill first (job-level entry point)
474
- meta_skill_path = self.generate_meta_skill(job, adapter, output_dir)
475
- skill_paths.append(meta_skill_path)
476
-
477
- # Generate step skills
478
- for step in job.steps:
479
- skill_path = self.generate_step_skill(job, step, adapter, output_dir, project_root_path)
480
- skill_paths.append(skill_path)
481
-
482
- return skill_paths
@@ -36,7 +36,7 @@ class HookEntry:
36
36
  """
37
37
  if self.module:
38
38
  # Python module - use deepwork hook CLI for portability
39
- # Extract hook name from module path (e.g., "deepwork.hooks.rules_check" -> "rules_check")
39
+ # Extract hook name from module path (e.g., "deepwork.hooks.my_hook" -> "my_hook")
40
40
  hook_name = self.module.rsplit(".", 1)[-1]
41
41
  return f"deepwork hook {hook_name}"
42
42
  elif self.script: