deepwork 0.1.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 (41) hide show
  1. deepwork/__init__.py +25 -0
  2. deepwork/cli/__init__.py +1 -0
  3. deepwork/cli/install.py +290 -0
  4. deepwork/cli/main.py +25 -0
  5. deepwork/cli/sync.py +176 -0
  6. deepwork/core/__init__.py +1 -0
  7. deepwork/core/adapters.py +373 -0
  8. deepwork/core/detector.py +93 -0
  9. deepwork/core/generator.py +290 -0
  10. deepwork/core/hooks_syncer.py +206 -0
  11. deepwork/core/parser.py +310 -0
  12. deepwork/core/policy_parser.py +285 -0
  13. deepwork/hooks/__init__.py +1 -0
  14. deepwork/hooks/evaluate_policies.py +159 -0
  15. deepwork/schemas/__init__.py +1 -0
  16. deepwork/schemas/job_schema.py +212 -0
  17. deepwork/schemas/policy_schema.py +68 -0
  18. deepwork/standard_jobs/deepwork_jobs/job.yml +102 -0
  19. deepwork/standard_jobs/deepwork_jobs/steps/define.md +359 -0
  20. deepwork/standard_jobs/deepwork_jobs/steps/implement.md +435 -0
  21. deepwork/standard_jobs/deepwork_jobs/steps/refine.md +447 -0
  22. deepwork/standard_jobs/deepwork_policy/hooks/capture_work_tree.sh +26 -0
  23. deepwork/standard_jobs/deepwork_policy/hooks/get_changed_files.sh +30 -0
  24. deepwork/standard_jobs/deepwork_policy/hooks/global_hooks.yml +8 -0
  25. deepwork/standard_jobs/deepwork_policy/hooks/policy_stop_hook.sh +72 -0
  26. deepwork/standard_jobs/deepwork_policy/hooks/user_prompt_submit.sh +17 -0
  27. deepwork/standard_jobs/deepwork_policy/job.yml +35 -0
  28. deepwork/standard_jobs/deepwork_policy/steps/define.md +174 -0
  29. deepwork/templates/__init__.py +1 -0
  30. deepwork/templates/claude/command-job-step.md.jinja +210 -0
  31. deepwork/templates/gemini/command-job-step.toml.jinja +169 -0
  32. deepwork/utils/__init__.py +1 -0
  33. deepwork/utils/fs.py +128 -0
  34. deepwork/utils/git.py +164 -0
  35. deepwork/utils/validation.py +31 -0
  36. deepwork/utils/yaml_utils.py +89 -0
  37. deepwork-0.1.0.dist-info/METADATA +389 -0
  38. deepwork-0.1.0.dist-info/RECORD +41 -0
  39. deepwork-0.1.0.dist-info/WHEEL +4 -0
  40. deepwork-0.1.0.dist-info/entry_points.txt +2 -0
  41. deepwork-0.1.0.dist-info/licenses/LICENSE.md +60 -0
@@ -0,0 +1,174 @@
1
+ # Define Policy
2
+
3
+ ## Objective
4
+
5
+ Create or update policy entries in the `.deepwork.policy.yml` file to enforce team guidelines, documentation requirements, or other constraints when specific files change.
6
+
7
+ ## Task
8
+
9
+ Guide the user through defining a new policy by asking clarifying questions. **Do not create the policy without first understanding what they want to enforce.**
10
+
11
+ ### Step 1: Understand the Policy Purpose
12
+
13
+ Start by asking questions to understand what the user wants to enforce:
14
+
15
+ 1. **What guideline or constraint should this policy enforce?**
16
+ - What situation triggers the need for action?
17
+ - What files or directories, when changed, should trigger this policy?
18
+ - Examples: "When config files change", "When API code changes", "When database schema changes"
19
+
20
+ 2. **What action should be taken?**
21
+ - What should the agent do when the policy triggers?
22
+ - Update documentation? Perform a security review? Update tests?
23
+ - Is there a specific file or process that needs attention?
24
+
25
+ 3. **Are there any "safety" conditions?**
26
+ - Are there files that, if also changed, mean the policy doesn't need to fire?
27
+ - For example: If config changes AND install_guide.md changes, assume docs are already updated
28
+ - This prevents redundant prompts when the user has already done the right thing
29
+
30
+ ### Step 2: Define the Trigger Patterns
31
+
32
+ Help the user define glob patterns for files that should trigger the policy:
33
+
34
+ **Common patterns:**
35
+ - `src/**/*.py` - All Python files in src directory (recursive)
36
+ - `app/config/**/*` - All files in app/config directory
37
+ - `*.md` - All markdown files in root
38
+ - `src/api/**/*` - All files in the API directory
39
+ - `migrations/**/*.sql` - All SQL migrations
40
+
41
+ **Pattern syntax:**
42
+ - `*` - Matches any characters within a single path segment
43
+ - `**` - Matches any characters across multiple path segments (recursive)
44
+ - `?` - Matches a single character
45
+
46
+ ### Step 3: Define Safety Patterns (Optional)
47
+
48
+ If there are files that, when also changed, mean the policy shouldn't fire:
49
+
50
+ **Examples:**
51
+ - Policy: "Update install guide when config changes"
52
+ - Trigger: `app/config/**/*`
53
+ - Safety: `docs/install_guide.md` (if already updated, don't prompt)
54
+
55
+ - Policy: "Security review for auth changes"
56
+ - Trigger: `src/auth/**/*`
57
+ - Safety: `SECURITY.md`, `docs/security_review.md`
58
+
59
+ ### Step 4: Write the Instructions
60
+
61
+ Create clear, actionable instructions for what the agent should do when the policy fires.
62
+
63
+ **Good instructions include:**
64
+ - What to check or review
65
+ - What files might need updating
66
+ - Specific actions to take
67
+ - Quality criteria for completion
68
+
69
+ **Example:**
70
+ ```
71
+ Configuration files have changed. Please:
72
+ 1. Review docs/install_guide.md for accuracy
73
+ 2. Update any installation steps that reference changed config
74
+ 3. Verify environment variable documentation is current
75
+ 4. Test that installation instructions still work
76
+ ```
77
+
78
+ ### Step 5: Create the Policy Entry
79
+
80
+ Create or update `.deepwork.policy.yml` in the project root.
81
+
82
+ **File Location**: `.deepwork.policy.yml` (root of project)
83
+
84
+ **Format**:
85
+ ```yaml
86
+ - name: "[Friendly name for the policy]"
87
+ trigger: "[glob pattern]" # or array: ["pattern1", "pattern2"]
88
+ safety: "[glob pattern]" # optional, or array
89
+ instructions: |
90
+ [Multi-line instructions for the agent...]
91
+ ```
92
+
93
+ **Alternative with instructions_file**:
94
+ ```yaml
95
+ - name: "[Friendly name for the policy]"
96
+ trigger: "[glob pattern]"
97
+ safety: "[glob pattern]"
98
+ instructions_file: "path/to/instructions.md"
99
+ ```
100
+
101
+ ### Step 6: Verify the Policy
102
+
103
+ After creating the policy:
104
+
105
+ 1. **Check the YAML syntax** - Ensure valid YAML formatting
106
+ 2. **Test trigger patterns** - Verify patterns match intended files
107
+ 3. **Review instructions** - Ensure they're clear and actionable
108
+ 4. **Check for conflicts** - Ensure the policy doesn't conflict with existing ones
109
+
110
+ ## Example Policies
111
+
112
+ ### Update Documentation on Config Changes
113
+ ```yaml
114
+ - name: "Update install guide on config changes"
115
+ trigger: "app/config/**/*"
116
+ safety: "docs/install_guide.md"
117
+ instructions: |
118
+ Configuration files have been modified. Please review docs/install_guide.md
119
+ and update it if any installation instructions need to change based on the
120
+ new configuration.
121
+ ```
122
+
123
+ ### Security Review for Auth Code
124
+ ```yaml
125
+ - name: "Security review for authentication changes"
126
+ trigger:
127
+ - "src/auth/**/*"
128
+ - "src/security/**/*"
129
+ safety:
130
+ - "SECURITY.md"
131
+ - "docs/security_audit.md"
132
+ instructions: |
133
+ Authentication or security code has been changed. Please:
134
+ 1. Review for hardcoded credentials or secrets
135
+ 2. Check input validation on user inputs
136
+ 3. Verify access control logic is correct
137
+ 4. Update security documentation if needed
138
+ ```
139
+
140
+ ### API Documentation Sync
141
+ ```yaml
142
+ - name: "API documentation update"
143
+ trigger: "src/api/**/*.py"
144
+ safety: "docs/api/**/*.md"
145
+ instructions: |
146
+ API code has changed. Please verify that API documentation in docs/api/
147
+ is up to date with the code changes. Pay special attention to:
148
+ - New or changed endpoints
149
+ - Modified request/response schemas
150
+ - Updated authentication requirements
151
+ ```
152
+
153
+ ## Output Format
154
+
155
+ ### .deepwork.policy.yml
156
+ Create or update this file at the project root with the new policy entry.
157
+
158
+ ## Quality Criteria
159
+
160
+ - Policy name is clear and descriptive
161
+ - Trigger patterns accurately match the intended files
162
+ - Safety patterns prevent unnecessary triggering
163
+ - Instructions are actionable and specific
164
+ - YAML is valid and properly formatted
165
+
166
+ ## Context
167
+
168
+ Policies are evaluated automatically when you finish working on a task. The system:
169
+ 1. Tracks which files you changed during the session
170
+ 2. Checks if any changes match policy trigger patterns
171
+ 3. Skips policies where safety patterns also matched
172
+ 4. Prompts you with instructions for any triggered policies
173
+
174
+ You can mark a policy as addressed by including `<promise>✓ Policy Name</promise>` in your response (replace Policy Name with the actual policy name). This tells the system you've already handled that policy's requirements.
@@ -0,0 +1 @@
1
+ """Skill templates for different AI platforms."""
@@ -0,0 +1,210 @@
1
+ ---
2
+ description: {{ step_description }}
3
+ {% if hooks %}
4
+ hooks:
5
+ {% for event_name, event_hooks in hooks.items() %}
6
+ {{ event_name }}:
7
+ - hooks:
8
+ {% for hook in event_hooks %}
9
+ {% if hook.type == "script" %}
10
+ - type: command
11
+ command: ".deepwork/jobs/{{ job_name }}/{{ hook.path }}"
12
+ {% else %}
13
+ - type: prompt
14
+ prompt: |
15
+ {% if event_name == "Stop" %}
16
+ You must evaluate whether Claude has met all the below quality criteria for the request.
17
+
18
+ ## Quality Criteria
19
+
20
+ {{ hook.content | indent(12) }}
21
+
22
+ ## Instructions
23
+
24
+ Review the conversation and determine if ALL quality criteria above have been satisfied.
25
+ Look for evidence that each criterion has been addressed.
26
+
27
+ If the agent has included `<promise>✓ Quality Criteria Met</promise>` in their response AND
28
+ all criteria appear to be met, respond with: {"ok": true}
29
+
30
+ If criteria are NOT met AND the promise tag is missing, respond with:
31
+ {"ok": false, "reason": "Continue working. [specific feedback on what's wrong]"}
32
+ {% else %}
33
+ {{ hook.content | indent(12) }}
34
+ {% endif %}
35
+ {% endif %}
36
+ {% endfor %}
37
+ {% endfor %}
38
+ {% endif %}
39
+ ---
40
+
41
+ # {{ job_name }}.{{ step_id }}
42
+
43
+ {% if is_standalone %}
44
+ **Standalone command** in the **{{ job_name }}** job - can be run anytime
45
+ {% else %}
46
+ **Step {{ step_number }} of {{ total_steps }}** in the **{{ job_name }}** workflow
47
+ {% endif %}
48
+
49
+ **Summary**: {{ job_summary }}
50
+
51
+ {% if job_description %}
52
+ ## Job Overview
53
+
54
+ {{ job_description }}
55
+ {% endif %}
56
+
57
+ {% if dependencies %}
58
+ ## Prerequisites
59
+
60
+ This step requires completion of the following step(s):
61
+ {% for dep in dependencies %}
62
+ - `/{{ job_name }}.{{ dep }}`
63
+ {% endfor %}
64
+
65
+ Please ensure these steps have been completed before proceeding.
66
+ {% endif %}
67
+
68
+ ## Instructions
69
+
70
+ {{ instructions_content }}
71
+
72
+ {% if user_inputs or file_inputs %}
73
+ ## Inputs
74
+
75
+ {% if user_inputs %}
76
+ ### User Parameters
77
+
78
+ Please gather the following information from the user:
79
+ {% for input in user_inputs %}
80
+ - **{{ input.name }}**: {{ input.description }}
81
+ {% endfor %}
82
+ {% endif %}
83
+
84
+ {% if file_inputs %}
85
+ ### Required Files
86
+
87
+ This step requires the following files from previous steps:
88
+ {% for input in file_inputs %}
89
+ - `{{ input.file }}` (from step `{{ input.from_step }}`)
90
+ {% endfor %}
91
+
92
+ Make sure to read and use these files as context for this step.
93
+ {% endif %}
94
+ {% endif %}
95
+
96
+ ## Work Branch Management
97
+
98
+ All work for this job should be done on a dedicated work branch:
99
+
100
+ 1. **Check current branch**:
101
+ - If already on a work branch for this job (format: `deepwork/{{ job_name }}-[instance]-[date]`), continue using it
102
+ - If on main/master, create a new work branch
103
+
104
+ 2. **Create work branch** (if needed):
105
+ ```bash
106
+ git checkout -b deepwork/{{ job_name }}-[instance]-$(date +%Y%m%d)
107
+ ```
108
+ Replace `[instance]` with a descriptive identifier (e.g., `acme`, `q1-launch`, etc.)
109
+
110
+ ## Output Requirements
111
+
112
+ {% if outputs %}
113
+ Create the following output(s):
114
+ {% for output in outputs %}
115
+ - `{{ output }}`{% if output.endswith('/') %} (directory){% endif %}
116
+ {% endfor %}
117
+
118
+ Ensure all outputs are:
119
+ - Well-formatted and complete
120
+ - Ready for review or use by subsequent steps
121
+ {% else %}
122
+ No specific files are output by this command.
123
+ {% endif %}
124
+
125
+ {% if stop_hooks %}
126
+ ## Quality Validation Loop
127
+
128
+ This step uses an iterative quality validation loop. After completing your work, stop hook(s) will evaluate whether the outputs meet quality criteria. If criteria are not met, you will be prompted to continue refining.
129
+
130
+ {% for hook in stop_hooks %}
131
+ {% if hook.type == "script" %}
132
+ **Validation Script**: `.deepwork/jobs/{{ job_name }}/{{ hook.path }}`
133
+
134
+ The validation script will be executed automatically when you attempt to complete this step.
135
+ {% else %}
136
+ ### Quality Criteria{% if stop_hooks | length > 1 %} ({{ loop.index }}){% endif %}
137
+
138
+ {{ hook.content }}
139
+ {% endif %}
140
+ {% endfor %}
141
+
142
+ ### Completion Promise
143
+
144
+ To signal that all quality criteria have been met, include this tag in your final response:
145
+
146
+ ```
147
+ <promise>✓ Quality Criteria Met</promise>
148
+ ```
149
+
150
+ **Important**: Only include this promise tag when you have verified that ALL quality criteria above are satisfied. The validation loop will continue until this promise is detected.
151
+
152
+ {% endif %}
153
+ ## Completion
154
+
155
+ After completing this step:
156
+
157
+ 1. **Verify outputs**: Confirm all required files have been created
158
+
159
+ 2. **Inform the user**:
160
+ {% if is_standalone %}
161
+ - The {{ step_id }} command is complete
162
+ {% if outputs %}
163
+ - Outputs created: {{ outputs | join(', ') }}
164
+ {% endif %}
165
+ - This command can be run again anytime to make further changes
166
+ {% else %}
167
+ - Step {{ step_number }} of {{ total_steps }} is complete
168
+ {% if outputs %}
169
+ - Outputs created: {{ outputs | join(', ') }}
170
+ {% endif %}
171
+ {% if next_step %}
172
+ - Ready to proceed to next step: `/{{ job_name }}.{{ next_step }}`
173
+ {% else %}
174
+ - This is the final step - the job is complete!
175
+ {% endif %}
176
+ {% endif %}
177
+
178
+ {% if is_standalone %}
179
+ ## Command Complete
180
+
181
+ This is a standalone command that can be run anytime. The outputs are ready for use.
182
+
183
+ Consider:
184
+ - Reviewing the outputs
185
+ - Running `deepwork sync` if job definitions were changed
186
+ - Re-running this command later if further changes are needed
187
+ {% elif next_step %}
188
+ ## Next Step
189
+
190
+ To continue the workflow, run:
191
+ ```
192
+ /{{ job_name }}.{{ next_step }}
193
+ ```
194
+ {% else %}
195
+ ## Workflow Complete
196
+
197
+ This is the final step in the {{ job_name }} workflow. All outputs should now be complete and ready for review.
198
+
199
+ Consider:
200
+ - Reviewing all work products
201
+ - Creating a pull request to merge the work branch
202
+ - Documenting any insights or learnings
203
+ {% endif %}
204
+
205
+ ---
206
+
207
+ ## Context Files
208
+
209
+ - Job definition: `.deepwork/jobs/{{ job_name }}/job.yml`
210
+ - Step instructions: `.deepwork/jobs/{{ job_name }}/{{ instructions_file }}`
@@ -0,0 +1,169 @@
1
+ # {{ job_name }}:{{ step_id }}
2
+ #
3
+ # {{ step_description }}
4
+ #
5
+ # Generated by DeepWork - do not edit manually
6
+
7
+ description = "{{ step_description | replace('"', '\\"') }}"
8
+
9
+ prompt = """
10
+ # {{ job_name }}:{{ step_id }}
11
+
12
+ {% if is_standalone %}
13
+ **Standalone command** in the **{{ job_name }}** job - can be run anytime
14
+ {% else %}
15
+ **Step {{ step_number }} of {{ total_steps }}** in the **{{ job_name }}** workflow
16
+ {% endif %}
17
+
18
+ **Summary**: {{ job_summary }}
19
+
20
+ {% if job_description %}
21
+ ## Job Overview
22
+
23
+ {{ job_description }}
24
+ {% endif %}
25
+
26
+ {% if dependencies %}
27
+ ## Prerequisites
28
+
29
+ This step requires completion of the following step(s):
30
+ {% for dep in dependencies %}
31
+ - `/{{ job_name }}:{{ dep }}`
32
+ {% endfor %}
33
+
34
+ Please ensure these steps have been completed before proceeding.
35
+ {% endif %}
36
+
37
+ ## Instructions
38
+
39
+ {{ instructions_content }}
40
+
41
+ {% if user_inputs or file_inputs %}
42
+ ## Inputs
43
+
44
+ {% if user_inputs %}
45
+ ### User Parameters
46
+
47
+ Please gather the following information from the user:
48
+ {% for input in user_inputs %}
49
+ - **{{ input.name }}**: {{ input.description }}
50
+ {% endfor %}
51
+ {% endif %}
52
+
53
+ {% if file_inputs %}
54
+ ### Required Files
55
+
56
+ This step requires the following files from previous steps:
57
+ {% for input in file_inputs %}
58
+ - `{{ input.file }}` (from step `{{ input.from_step }}`)
59
+ {% endfor %}
60
+
61
+ Make sure to read and use these files as context for this step.
62
+ {% endif %}
63
+ {% endif %}
64
+
65
+ ## Work Branch Management
66
+
67
+ All work for this job should be done on a dedicated work branch:
68
+
69
+ 1. **Check current branch**:
70
+ - If already on a work branch for this job (format: `deepwork/{{ job_name }}-[instance]-[date]`), continue using it
71
+ - If on main/master, create a new work branch
72
+
73
+ 2. **Create work branch** (if needed):
74
+ ```bash
75
+ git checkout -b deepwork/{{ job_name }}-[instance]-$(date +%Y%m%d)
76
+ ```
77
+ Replace `[instance]` with a descriptive identifier (e.g., `acme`, `q1-launch`, etc.)
78
+
79
+ ## Output Requirements
80
+
81
+ {% if outputs %}
82
+ Create the following output(s):
83
+ {% for output in outputs %}
84
+ - `{{ output }}`{% if output.endswith('/') %} (directory){% endif %}
85
+
86
+ {% endfor %}
87
+
88
+ Ensure all outputs are:
89
+ - Well-formatted and complete
90
+ - Ready for review or use by subsequent steps
91
+ {% else %}
92
+ No specific files are output by this command.
93
+ {% endif %}
94
+
95
+ {% if stop_hooks %}
96
+ ## Quality Validation
97
+
98
+ This step has quality criteria that should be verified before completion.
99
+
100
+ {% for hook in stop_hooks %}
101
+ {% if hook.type != "script" %}
102
+ ### Quality Criteria{% if stop_hooks | length > 1 %} ({{ loop.index }}){% endif %}
103
+
104
+ {{ hook.content }}
105
+ {% endif %}
106
+ {% endfor %}
107
+
108
+ **Note**: Gemini CLI does not support automated validation hooks. Please manually verify the criteria above before proceeding.
109
+
110
+ {% endif %}
111
+ ## Completion
112
+
113
+ After completing this step:
114
+
115
+ 1. **Verify outputs**: Confirm all required files have been created
116
+
117
+ 2. **Inform the user**:
118
+ {% if is_standalone %}
119
+ - The {{ step_id }} command is complete
120
+ {% if outputs %}
121
+ - Outputs created: {{ outputs | join(', ') }}
122
+ {% endif %}
123
+ - This command can be run again anytime to make further changes
124
+ {% else %}
125
+ - Step {{ step_number }} of {{ total_steps }} is complete
126
+ {% if outputs %}
127
+ - Outputs created: {{ outputs | join(', ') }}
128
+ {% endif %}
129
+ {% if next_step %}
130
+ - Ready to proceed to next step: `/{{ job_name }}:{{ next_step }}`
131
+ {% else %}
132
+ - This is the final step - the job is complete!
133
+ {% endif %}
134
+ {% endif %}
135
+
136
+ {% if is_standalone %}
137
+ ## Command Complete
138
+
139
+ This is a standalone command that can be run anytime. The outputs are ready for use.
140
+
141
+ Consider:
142
+ - Reviewing the outputs
143
+ - Running `deepwork sync` if job definitions were changed
144
+ - Re-running this command later if further changes are needed
145
+ {% elif next_step %}
146
+ ## Next Step
147
+
148
+ To continue the workflow, run:
149
+ ```
150
+ /{{ job_name }}:{{ next_step }}
151
+ ```
152
+ {% else %}
153
+ ## Workflow Complete
154
+
155
+ This is the final step in the {{ job_name }} workflow. All outputs should now be complete and ready for review.
156
+
157
+ Consider:
158
+ - Reviewing all work products
159
+ - Creating a pull request to merge the work branch
160
+ - Documenting any insights or learnings
161
+ {% endif %}
162
+
163
+ ---
164
+
165
+ ## Context Files
166
+
167
+ - Job definition: `.deepwork/jobs/{{ job_name }}/job.yml`
168
+ - Step instructions: `.deepwork/jobs/{{ job_name }}/{{ instructions_file }}`
169
+ """
@@ -0,0 +1 @@
1
+ """Utility functions for DeepWork."""
deepwork/utils/fs.py ADDED
@@ -0,0 +1,128 @@
1
+ """Filesystem utilities for safe file operations."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+
7
+ def ensure_dir(path: Path | str) -> Path:
8
+ """
9
+ Create directory if it doesn't exist.
10
+
11
+ Args:
12
+ path: Directory path to create
13
+
14
+ Returns:
15
+ Path object for the created/existing directory
16
+ """
17
+ path_obj = Path(path)
18
+ path_obj.mkdir(parents=True, exist_ok=True)
19
+ return path_obj
20
+
21
+
22
+ def safe_write(path: Path | str, content: str) -> None:
23
+ """
24
+ Write content to file, creating parent directories if needed.
25
+
26
+ Args:
27
+ path: File path to write to
28
+ content: Content to write
29
+
30
+ Raises:
31
+ OSError: If write operation fails
32
+ """
33
+ path_obj = Path(path)
34
+ ensure_dir(path_obj.parent)
35
+ path_obj.write_text(content, encoding="utf-8")
36
+
37
+
38
+ def safe_read(path: Path | str) -> str | None:
39
+ """
40
+ Read content from file, return None if file doesn't exist.
41
+
42
+ Args:
43
+ path: File path to read from
44
+
45
+ Returns:
46
+ File content as string, or None if file doesn't exist
47
+
48
+ Raises:
49
+ OSError: If read operation fails for reasons other than file not existing
50
+ """
51
+ path_obj = Path(path)
52
+ if not path_obj.exists():
53
+ return None
54
+ return path_obj.read_text(encoding="utf-8")
55
+
56
+
57
+ def copy_dir(src: Path | str, dst: Path | str, ignore_patterns: list[str] | None = None) -> None:
58
+ """
59
+ Recursively copy directory, optionally ignoring patterns.
60
+
61
+ Args:
62
+ src: Source directory path
63
+ dst: Destination directory path
64
+ ignore_patterns: Optional list of glob patterns to ignore
65
+
66
+ Raises:
67
+ FileNotFoundError: If source directory doesn't exist
68
+ OSError: If copy operation fails
69
+ """
70
+ src_path = Path(src)
71
+ dst_path = Path(dst)
72
+
73
+ if not src_path.exists():
74
+ raise FileNotFoundError(f"Source directory does not exist: {src_path}")
75
+
76
+ if not src_path.is_dir():
77
+ raise NotADirectoryError(f"Source is not a directory: {src_path}")
78
+
79
+ # Create ignore function if patterns provided
80
+ ignore_func = None
81
+ if ignore_patterns:
82
+
83
+ def _ignore(directory: str, contents: list[str]) -> set[str]:
84
+ ignored = set()
85
+ dir_path = Path(directory)
86
+ for item in contents:
87
+ item_path = dir_path / item
88
+ for pattern in ignore_patterns:
89
+ if item_path.match(pattern):
90
+ ignored.add(item)
91
+ break
92
+ return ignored
93
+
94
+ ignore_func = _ignore
95
+
96
+ shutil.copytree(src_path, dst_path, ignore=ignore_func, dirs_exist_ok=True)
97
+
98
+
99
+ def find_files(directory: Path | str, pattern: str) -> list[Path]:
100
+ """
101
+ Find files matching glob pattern in directory.
102
+
103
+ Args:
104
+ directory: Directory to search in
105
+ pattern: Glob pattern to match (e.g., "*.py", "**/*.md")
106
+
107
+ Returns:
108
+ List of matching file paths (sorted)
109
+
110
+ Raises:
111
+ FileNotFoundError: If directory doesn't exist
112
+ """
113
+ dir_path = Path(directory)
114
+
115
+ if not dir_path.exists():
116
+ raise FileNotFoundError(f"Directory does not exist: {dir_path}")
117
+
118
+ if not dir_path.is_dir():
119
+ raise NotADirectoryError(f"Path is not a directory: {dir_path}")
120
+
121
+ # Use rglob for ** patterns, otherwise use glob
122
+ if "**" in pattern:
123
+ matches = dir_path.glob(pattern)
124
+ else:
125
+ matches = dir_path.glob(pattern)
126
+
127
+ # Return only files, not directories, sorted by path
128
+ return sorted([p for p in matches if p.is_file()])