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
@@ -0,0 +1,363 @@
1
+ """
2
+ Hook wrapper module for cross-platform hook compatibility.
3
+
4
+ This module provides utilities for normalizing hook input/output between
5
+ different AI CLI platforms (Claude Code, Gemini CLI, etc.).
6
+
7
+ The wrapper system allows writing hooks once in Python and running them
8
+ on any supported platform. Platform-specific shell scripts handle the
9
+ input/output translation, while Python hooks work with a normalized format.
10
+
11
+ Normalized Format:
12
+ Input:
13
+ - session_id: str
14
+ - transcript_path: str
15
+ - cwd: str
16
+ - event: str (normalized: 'after_agent', 'before_tool', 'before_prompt')
17
+ - tool_name: str (normalized: 'write_file', 'shell', etc.)
18
+ - tool_input: dict
19
+ - prompt: str (for agent events)
20
+ - raw_input: dict (original platform-specific input)
21
+
22
+ Output:
23
+ - decision: str ('block', 'allow', 'deny')
24
+ - reason: str (explanation for blocking)
25
+ - context: str (additional context to add)
26
+ - raw_output: dict (will be merged into final output)
27
+
28
+ Usage:
29
+ # In a Python hook:
30
+ from deepwork.hooks.wrapper import HookInput, HookOutput, normalize_input, denormalize_output
31
+
32
+ def my_hook(input_data: HookInput) -> HookOutput:
33
+ if should_block:
34
+ return HookOutput(decision='block', reason='Must do X first')
35
+ return HookOutput() # Allow
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import json
41
+ import sys
42
+ from collections.abc import Callable
43
+ from dataclasses import dataclass, field
44
+ from enum import Enum
45
+ from typing import Any
46
+
47
+
48
+ class Platform(str, Enum):
49
+ """Supported AI CLI platforms."""
50
+
51
+ CLAUDE = "claude"
52
+ GEMINI = "gemini"
53
+
54
+
55
+ class NormalizedEvent(str, Enum):
56
+ """Normalized hook event names."""
57
+
58
+ AFTER_AGENT = "after_agent"
59
+ BEFORE_TOOL = "before_tool"
60
+ BEFORE_PROMPT = "before_prompt"
61
+ SESSION_START = "session_start"
62
+ SESSION_END = "session_end"
63
+ AFTER_TOOL = "after_tool"
64
+ BEFORE_MODEL = "before_model"
65
+ AFTER_MODEL = "after_model"
66
+
67
+
68
+ # Event name mappings from platform-specific to normalized
69
+ EVENT_TO_NORMALIZED: dict[Platform, dict[str, NormalizedEvent]] = {
70
+ Platform.CLAUDE: {
71
+ "Stop": NormalizedEvent.AFTER_AGENT,
72
+ "SubagentStop": NormalizedEvent.AFTER_AGENT,
73
+ "PreToolUse": NormalizedEvent.BEFORE_TOOL,
74
+ "PostToolUse": NormalizedEvent.AFTER_TOOL,
75
+ "UserPromptSubmit": NormalizedEvent.BEFORE_PROMPT,
76
+ "SessionStart": NormalizedEvent.SESSION_START,
77
+ "SessionEnd": NormalizedEvent.SESSION_END,
78
+ },
79
+ Platform.GEMINI: {
80
+ "AfterAgent": NormalizedEvent.AFTER_AGENT,
81
+ "BeforeTool": NormalizedEvent.BEFORE_TOOL,
82
+ "AfterTool": NormalizedEvent.AFTER_TOOL,
83
+ "BeforeAgent": NormalizedEvent.BEFORE_PROMPT,
84
+ "SessionStart": NormalizedEvent.SESSION_START,
85
+ "SessionEnd": NormalizedEvent.SESSION_END,
86
+ "BeforeModel": NormalizedEvent.BEFORE_MODEL,
87
+ "AfterModel": NormalizedEvent.AFTER_MODEL,
88
+ },
89
+ }
90
+
91
+ # Normalized event to platform-specific event name
92
+ NORMALIZED_TO_EVENT: dict[Platform, dict[NormalizedEvent, str]] = {
93
+ Platform.CLAUDE: {
94
+ NormalizedEvent.AFTER_AGENT: "Stop",
95
+ NormalizedEvent.BEFORE_TOOL: "PreToolUse",
96
+ NormalizedEvent.AFTER_TOOL: "PostToolUse",
97
+ NormalizedEvent.BEFORE_PROMPT: "UserPromptSubmit",
98
+ NormalizedEvent.SESSION_START: "SessionStart",
99
+ NormalizedEvent.SESSION_END: "SessionEnd",
100
+ },
101
+ Platform.GEMINI: {
102
+ NormalizedEvent.AFTER_AGENT: "AfterAgent",
103
+ NormalizedEvent.BEFORE_TOOL: "BeforeTool",
104
+ NormalizedEvent.AFTER_TOOL: "AfterTool",
105
+ NormalizedEvent.BEFORE_PROMPT: "BeforeAgent",
106
+ NormalizedEvent.SESSION_START: "SessionStart",
107
+ NormalizedEvent.SESSION_END: "SessionEnd",
108
+ NormalizedEvent.BEFORE_MODEL: "BeforeModel",
109
+ NormalizedEvent.AFTER_MODEL: "AfterModel",
110
+ },
111
+ }
112
+
113
+ # Tool name mappings from platform-specific to normalized (snake_case)
114
+ TOOL_TO_NORMALIZED: dict[Platform, dict[str, str]] = {
115
+ Platform.CLAUDE: {
116
+ "Write": "write_file",
117
+ "Edit": "edit_file",
118
+ "Read": "read_file",
119
+ "Bash": "shell",
120
+ "Glob": "glob",
121
+ "Grep": "grep",
122
+ "WebFetch": "web_fetch",
123
+ "WebSearch": "web_search",
124
+ "Task": "task",
125
+ },
126
+ Platform.GEMINI: {
127
+ # Gemini already uses snake_case
128
+ "write_file": "write_file",
129
+ "edit_file": "edit_file",
130
+ "read_file": "read_file",
131
+ "shell": "shell",
132
+ "glob": "glob",
133
+ "grep": "grep",
134
+ "web_fetch": "web_fetch",
135
+ "web_search": "web_search",
136
+ },
137
+ }
138
+
139
+ # Normalized tool names to platform-specific
140
+ NORMALIZED_TO_TOOL: dict[Platform, dict[str, str]] = {
141
+ Platform.CLAUDE: {
142
+ "write_file": "Write",
143
+ "edit_file": "Edit",
144
+ "read_file": "Read",
145
+ "shell": "Bash",
146
+ "glob": "Glob",
147
+ "grep": "Grep",
148
+ "web_fetch": "WebFetch",
149
+ "web_search": "WebSearch",
150
+ "task": "Task",
151
+ },
152
+ Platform.GEMINI: {
153
+ # Gemini already uses snake_case
154
+ "write_file": "write_file",
155
+ "edit_file": "edit_file",
156
+ "read_file": "read_file",
157
+ "shell": "shell",
158
+ "glob": "glob",
159
+ "grep": "grep",
160
+ "web_fetch": "web_fetch",
161
+ "web_search": "web_search",
162
+ },
163
+ }
164
+
165
+
166
+ @dataclass
167
+ class HookInput:
168
+ """Normalized hook input data."""
169
+
170
+ platform: Platform
171
+ event: NormalizedEvent
172
+ session_id: str = ""
173
+ transcript_path: str = ""
174
+ cwd: str = ""
175
+ tool_name: str = ""
176
+ tool_input: dict[str, Any] = field(default_factory=dict)
177
+ tool_response: str = ""
178
+ prompt: str = ""
179
+ raw_input: dict[str, Any] = field(default_factory=dict)
180
+
181
+ @classmethod
182
+ def from_dict(cls, data: dict[str, Any], platform: Platform) -> HookInput:
183
+ """Create HookInput from raw platform-specific input."""
184
+ # Get event name and normalize
185
+ raw_event = data.get("hook_event_name", "")
186
+ event_map = EVENT_TO_NORMALIZED.get(platform, {})
187
+ event = event_map.get(raw_event, NormalizedEvent.AFTER_AGENT)
188
+
189
+ # Get tool name and normalize
190
+ raw_tool = data.get("tool_name", "")
191
+ tool_map = TOOL_TO_NORMALIZED.get(platform, {})
192
+ tool_name = tool_map.get(raw_tool, raw_tool.lower())
193
+
194
+ return cls(
195
+ platform=platform,
196
+ event=event,
197
+ session_id=data.get("session_id", ""),
198
+ transcript_path=data.get("transcript_path", ""),
199
+ cwd=data.get("cwd", ""),
200
+ tool_name=tool_name,
201
+ tool_input=data.get("tool_input", {}),
202
+ tool_response=data.get("tool_response", ""),
203
+ prompt=data.get("prompt", ""),
204
+ raw_input=data,
205
+ )
206
+
207
+
208
+ @dataclass
209
+ class HookOutput:
210
+ """Normalized hook output data."""
211
+
212
+ decision: str = "" # 'block', 'allow', 'deny', '' (empty = allow)
213
+ reason: str = "" # Explanation for blocking
214
+ context: str = "" # Additional context to add
215
+ continue_loop: bool = True # False to terminate agent loop
216
+ stop_reason: str = "" # Message when stopping
217
+ suppress_output: bool = False # Hide from transcript
218
+ raw_output: dict[str, Any] = field(default_factory=dict)
219
+
220
+ def to_dict(self, platform: Platform, event: NormalizedEvent) -> dict[str, Any]:
221
+ """Convert to platform-specific output format."""
222
+ result: dict[str, Any] = {}
223
+
224
+ # Handle decision
225
+ if self.decision:
226
+ if platform == Platform.GEMINI and self.decision == "block":
227
+ # Gemini prefers 'deny'
228
+ result["decision"] = "deny"
229
+ else:
230
+ result["decision"] = self.decision
231
+
232
+ # Handle reason
233
+ if self.reason:
234
+ result["reason"] = self.reason
235
+
236
+ # Handle continue_loop
237
+ if not self.continue_loop:
238
+ result["continue"] = False
239
+ if self.stop_reason:
240
+ result["stopReason"] = self.stop_reason
241
+
242
+ # Handle suppress_output
243
+ if self.suppress_output:
244
+ result["suppressOutput"] = True
245
+
246
+ # Handle context (platform-specific)
247
+ if self.context:
248
+ if platform == Platform.CLAUDE:
249
+ # Claude uses different fields depending on event
250
+ if event == NormalizedEvent.SESSION_START:
251
+ result.setdefault("hookSpecificOutput", {})
252
+ result["hookSpecificOutput"]["hookEventName"] = NORMALIZED_TO_EVENT[platform][
253
+ event
254
+ ]
255
+ result["hookSpecificOutput"]["additionalContext"] = self.context
256
+ else:
257
+ result["systemMessage"] = self.context
258
+ else:
259
+ # Gemini
260
+ result.setdefault("hookSpecificOutput", {})
261
+ result["hookSpecificOutput"]["hookEventName"] = NORMALIZED_TO_EVENT[platform].get(
262
+ event, str(event)
263
+ )
264
+ result["hookSpecificOutput"]["additionalContext"] = self.context
265
+
266
+ # Merge any raw output
267
+ for key, value in self.raw_output.items():
268
+ if key not in result:
269
+ result[key] = value
270
+
271
+ return result
272
+
273
+
274
+ def normalize_input(raw_json: str, platform: Platform) -> HookInput:
275
+ """
276
+ Parse raw JSON input and normalize it.
277
+
278
+ Args:
279
+ raw_json: JSON string from stdin
280
+ platform: Source platform
281
+
282
+ Returns:
283
+ Normalized HookInput
284
+ """
285
+ try:
286
+ data = json.loads(raw_json) if raw_json.strip() else {}
287
+ except json.JSONDecodeError:
288
+ data = {}
289
+
290
+ return HookInput.from_dict(data, platform)
291
+
292
+
293
+ def denormalize_output(output: HookOutput, platform: Platform, event: NormalizedEvent) -> str:
294
+ """
295
+ Convert normalized output to platform-specific JSON.
296
+
297
+ Args:
298
+ output: Normalized HookOutput
299
+ platform: Target platform
300
+ event: The event being processed
301
+
302
+ Returns:
303
+ JSON string for stdout
304
+ """
305
+ result = output.to_dict(platform, event)
306
+ return json.dumps(result) if result else "{}"
307
+
308
+
309
+ def read_stdin() -> str:
310
+ """Read all input from stdin."""
311
+ if sys.stdin.isatty():
312
+ return ""
313
+ try:
314
+ return sys.stdin.read()
315
+ except Exception:
316
+ return ""
317
+
318
+
319
+ def write_stdout(data: str) -> None:
320
+ """Write output to stdout."""
321
+ print(data)
322
+
323
+
324
+ def run_hook(
325
+ hook_fn: Callable[[HookInput], HookOutput],
326
+ platform: Platform,
327
+ ) -> int:
328
+ """
329
+ Run a hook function with normalized input/output.
330
+
331
+ This is the main entry point for Python hooks. It:
332
+ 1. Reads raw input from stdin
333
+ 2. Normalizes the input
334
+ 3. Calls the hook function
335
+ 4. Denormalizes the output
336
+ 5. Writes to stdout
337
+
338
+ Args:
339
+ hook_fn: Function that takes HookInput and returns HookOutput
340
+ platform: The platform calling this hook
341
+
342
+ Returns:
343
+ Exit code (0 for success, 2 for blocking)
344
+ """
345
+ # Read and normalize input
346
+ raw_input = read_stdin()
347
+ hook_input = normalize_input(raw_input, platform)
348
+
349
+ # Call the hook
350
+ try:
351
+ hook_output = hook_fn(hook_input)
352
+ except Exception as e:
353
+ # On error, allow the action but log
354
+ print(f"Hook error: {e}", file=sys.stderr)
355
+ hook_output = HookOutput()
356
+
357
+ # Denormalize and write output
358
+ output_json = denormalize_output(hook_output, platform, hook_input.event)
359
+ write_stdout(output_json)
360
+
361
+ # Always return 0 when using JSON output format
362
+ # The decision field in the JSON controls blocking behavior
363
+ return 0
@@ -3,7 +3,7 @@
3
3
  from typing import Any
4
4
 
5
5
  # Supported lifecycle hook events (generic names, mapped to platform-specific by adapters)
6
- # These values must match CommandLifecycleHook enum in adapters.py
6
+ # These values must match SkillLifecycleHook enum in adapters.py
7
7
  LIFECYCLE_HOOK_EVENTS = ["after_agent", "before_tool", "before_prompt"]
8
8
 
9
9
  # Schema definition for a single hook action (prompt, prompt_file, or script)
@@ -203,6 +203,19 @@ JOB_SCHEMA: dict[str, Any] = {
203
203
  "description": "DEPRECATED: Use hooks.after_agent instead. Stop hooks for quality validation loops.",
204
204
  "items": HOOK_ACTION_SCHEMA,
205
205
  },
206
+ "exposed": {
207
+ "type": "boolean",
208
+ "description": "If true, skill is user-invocable in menus. Default: false (hidden from menus).",
209
+ "default": False,
210
+ },
211
+ "quality_criteria": {
212
+ "type": "array",
213
+ "description": "Declarative quality criteria. Rendered with standard evaluation framing.",
214
+ "items": {
215
+ "type": "string",
216
+ "minLength": 1,
217
+ },
218
+ },
206
219
  },
207
220
  "additionalProperties": False,
208
221
  },
@@ -0,0 +1,103 @@
1
+ """JSON Schema definition for rule definitions (v2 - frontmatter format)."""
2
+
3
+ from typing import Any
4
+
5
+ # Pattern for string or array of strings
6
+ STRING_OR_ARRAY: dict[str, Any] = {
7
+ "oneOf": [
8
+ {"type": "string", "minLength": 1},
9
+ {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 1},
10
+ ]
11
+ }
12
+
13
+ # JSON Schema for rule frontmatter (YAML between --- delimiters)
14
+ # Rules are stored as individual .md files in .deepwork/rules/
15
+ RULES_FRONTMATTER_SCHEMA: dict[str, Any] = {
16
+ "$schema": "http://json-schema.org/draft-07/schema#",
17
+ "type": "object",
18
+ "required": ["name"],
19
+ "properties": {
20
+ "name": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "description": "Human-friendly name for the rule (displayed in promise tags)",
24
+ },
25
+ # Detection mode: trigger/safety (mutually exclusive with set/pair)
26
+ "trigger": {
27
+ **STRING_OR_ARRAY,
28
+ "description": "Glob pattern(s) for files that trigger this rule",
29
+ },
30
+ "safety": {
31
+ **STRING_OR_ARRAY,
32
+ "description": "Glob pattern(s) that suppress the rule if changed",
33
+ },
34
+ # Detection mode: set (bidirectional correspondence)
35
+ "set": {
36
+ "type": "array",
37
+ "items": {"type": "string", "minLength": 1},
38
+ "minItems": 2,
39
+ "description": "Patterns defining bidirectional file correspondence",
40
+ },
41
+ # Detection mode: pair (directional correspondence)
42
+ "pair": {
43
+ "type": "object",
44
+ "required": ["trigger", "expects"],
45
+ "properties": {
46
+ "trigger": {
47
+ "type": "string",
48
+ "minLength": 1,
49
+ "description": "Pattern that triggers the rule",
50
+ },
51
+ "expects": {
52
+ **STRING_OR_ARRAY,
53
+ "description": "Pattern(s) for expected corresponding files",
54
+ },
55
+ },
56
+ "additionalProperties": False,
57
+ "description": "Directional file correspondence (trigger -> expects)",
58
+ },
59
+ # Action type: command (default is prompt using markdown body)
60
+ "action": {
61
+ "type": "object",
62
+ "required": ["command"],
63
+ "properties": {
64
+ "command": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "description": "Command to run (supports {file}, {files}, {repo_root})",
68
+ },
69
+ "run_for": {
70
+ "type": "string",
71
+ "enum": ["each_match", "all_matches"],
72
+ "default": "each_match",
73
+ "description": "Run command for each file or all files at once",
74
+ },
75
+ },
76
+ "additionalProperties": False,
77
+ "description": "Command action to run instead of prompting",
78
+ },
79
+ # Common options
80
+ "compare_to": {
81
+ "type": "string",
82
+ "enum": ["base", "default_tip", "prompt"],
83
+ "default": "base",
84
+ "description": "Baseline for detecting file changes",
85
+ },
86
+ },
87
+ "additionalProperties": False,
88
+ # Detection mode must be exactly one of: trigger, set, or pair
89
+ "oneOf": [
90
+ {
91
+ "required": ["trigger"],
92
+ "not": {"anyOf": [{"required": ["set"]}, {"required": ["pair"]}]},
93
+ },
94
+ {
95
+ "required": ["set"],
96
+ "not": {"anyOf": [{"required": ["trigger"]}, {"required": ["pair"]}]},
97
+ },
98
+ {
99
+ "required": ["pair"],
100
+ "not": {"anyOf": [{"required": ["trigger"]}, {"required": ["set"]}]},
101
+ },
102
+ ],
103
+ }
@@ -0,0 +1,60 @@
1
+ # Project Context for deepwork_jobs
2
+
3
+ This is the source of truth for the `deepwork_jobs` standard job.
4
+
5
+ ## Codebase Structure
6
+
7
+ - Source location: `src/deepwork/standard_jobs/deepwork_jobs/`
8
+ - Working copy: `.deepwork/jobs/deepwork_jobs/`
9
+ - Templates: `templates/` directory within each location
10
+
11
+ ## Dual Location Maintenance
12
+
13
+ **Important**: This job exists in two locations that must be kept in sync:
14
+
15
+ 1. **Source of truth**: `src/deepwork/standard_jobs/deepwork_jobs/`
16
+ - This is where changes should be made first
17
+ - Tracked in version control
18
+
19
+ 2. **Working copy**: `.deepwork/jobs/deepwork_jobs/`
20
+ - Must be updated after changes to source
21
+ - Used by `deepwork sync` to generate commands
22
+
23
+ After making changes to the source, copy files to the working copy:
24
+ ```bash
25
+ cp src/deepwork/standard_jobs/deepwork_jobs/job.yml .deepwork/jobs/deepwork_jobs/
26
+ cp src/deepwork/standard_jobs/deepwork_jobs/steps/*.md .deepwork/jobs/deepwork_jobs/steps/
27
+ cp -r src/deepwork/standard_jobs/deepwork_jobs/templates/* .deepwork/jobs/deepwork_jobs/templates/
28
+ ```
29
+
30
+ ## File Organization
31
+
32
+ ```
33
+ deepwork_jobs/
34
+ ├── AGENTS.md # This file
35
+ ├── job.yml # Job definition
36
+ ├── make_new_job.sh # Script to create new job structure
37
+ ├── steps/
38
+ │ ├── define.md # Define step instructions
39
+ │ ├── implement.md # Implement step instructions
40
+ │ ├── learn.md # Learn step instructions
41
+ │ └── supplemental_file_references.md # Reference documentation
42
+ └── templates/
43
+ ├── job.yml.template # Job spec structure
44
+ ├── step_instruction.md.template # Step instruction structure
45
+ ├── agents.md.template # AGENTS.md structure
46
+ ├── job.yml.example # Complete job example
47
+ └── step_instruction.md.example # Complete step example
48
+ ```
49
+
50
+ ## Version Management
51
+
52
+ - Version is tracked in `job.yml`
53
+ - Bump patch version (0.0.x) for instruction improvements
54
+ - Bump minor version (0.x.0) for new features or structural changes
55
+ - Always update changelog when bumping version
56
+
57
+ ## Last Updated
58
+
59
+ - Date: 2026-01-15
60
+ - From conversation about: Adding make_new_job.sh script and templates directory