deepwork 0.2.0__py3-none-any.whl → 0.3.1__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 +116 -71
- 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 +559 -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 +700 -0
- deepwork/hooks/wrapper.py +363 -0
- deepwork/schemas/job_schema.py +14 -1
- deepwork/schemas/rules_schema.py +135 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +35 -53
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +9 -6
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +28 -26
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +2 -2
- deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +30 -0
- deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml +8 -0
- deepwork/standard_jobs/deepwork_rules/job.yml +47 -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 +46 -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.2.0.dist-info → deepwork-0.3.1.dist-info}/METADATA +56 -25
- deepwork-0.3.1.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/capture_prompt_work_tree.sh +0 -27
- 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/default_policy.yml +0 -53
- deepwork/templates/gemini/command-job-step.toml.jinja +0 -169
- deepwork-0.2.0.dist-info/RECORD +0 -49
- /deepwork/standard_jobs/{deepwork_policy → deepwork_rules}/hooks/user_prompt_submit.sh +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/WHEEL +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/entry_points.txt +0 -0
- {deepwork-0.2.0.dist-info → deepwork-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
deepwork/__init__.py,sha256=vcMnJioxhfoL6kGh4FM51Vk9UoLnQ76g6Ms7XDUItYA,748
|
|
2
|
+
deepwork/cli/__init__.py,sha256=3SqmfcP2xqutiCYAbajFDJTjr2pOLydqTN0NN-FTsIE,33
|
|
3
|
+
deepwork/cli/install.py,sha256=USSmALGWpCBc4kinAxTbt8j2AcSJ4vVNtYpG5s0M0sE,12897
|
|
4
|
+
deepwork/cli/main.py,sha256=m6tVnfSy03azI9Ky6ySEMat_UdLEMzVT3SsRKQWigvA,471
|
|
5
|
+
deepwork/cli/sync.py,sha256=d608WDOzOVpYbS6RF-xoonK3qj22WyewRFJrva_qXaQ,6251
|
|
6
|
+
deepwork/core/__init__.py,sha256=1g869QuwsYzNjQONneng2OMc6HKt-tlBCaxJbMMfoho,36
|
|
7
|
+
deepwork/core/adapters.py,sha256=OeFw4Db6nEKnSDLgol-mhYTx-GV9XEBp_4qI1WuX7KA,14410
|
|
8
|
+
deepwork/core/command_executor.py,sha256=l2XZ9EpCPY6dWqCPGUjq2jczq154evp2IvPBO4O6Xpg,4733
|
|
9
|
+
deepwork/core/detector.py,sha256=PThpFLH-ZVL8UqjVdGSnGA0mFbhc6_rp6V3_Yzw97kI,2466
|
|
10
|
+
deepwork/core/generator.py,sha256=0tauPRUA_D9o6BEksx9BJFV9lOlpgK1Te8fI2gxP0kc,14186
|
|
11
|
+
deepwork/core/hooks_syncer.py,sha256=kEr8bLgWJXPb1C8AOv2fxUnIX1j9zgfPahyYIZUvfko,6771
|
|
12
|
+
deepwork/core/parser.py,sha256=R799f_IC9zhQNROnIC534LRZ_TMYp5gaqwH3nGSrI9Y,9936
|
|
13
|
+
deepwork/core/pattern_matcher.py,sha256=wtrxFNSCMxk-47Nw4Ca-8769ll4weRvvQBLGsRERcKM,8073
|
|
14
|
+
deepwork/core/rules_parser.py,sha256=r_Pay0Ug9FDGCEccpLnQjsQjSEUmECceh0Ac3Ayw4oE,17774
|
|
15
|
+
deepwork/core/rules_queue.py,sha256=uCNuQzohUyEYGnCUv1nMDEYoLeTuiSHN44-kuXbSLDU,10069
|
|
16
|
+
deepwork/hooks/README.md,sha256=t4rAd68oRx0-cbln4kfV0Sk2IWhGAGR7HB4McgBXTYE,4391
|
|
17
|
+
deepwork/hooks/__init__.py,sha256=aUWXT5b8MaKzYp7x2Bwezj_NQwAJ-6a8nXxDz0o0qsY,1882
|
|
18
|
+
deepwork/hooks/claude_hook.sh,sha256=VjbRVOUirIhFU7CcsafWk25HTIUzZmCrWqNPiY9e17o,1539
|
|
19
|
+
deepwork/hooks/gemini_hook.sh,sha256=wVfnZs7VvUNFgOkZnhUpbMYlVyy7lQWG_u8PqDQbL8g,1534
|
|
20
|
+
deepwork/hooks/rules_check.py,sha256=dmXnxOkpT1KH32uQkYeb_Btwykykb4-Gr4H4C86IIfo,23562
|
|
21
|
+
deepwork/hooks/wrapper.py,sha256=mSps8tWQEdBThXwDViOtNad0d9pFaNPF8MFpG-Rhuds,11517
|
|
22
|
+
deepwork/schemas/__init__.py,sha256=PpydKb_oaTv8lYapN_nV-Tl_OUCoSM_okvsEJ8gNTpI,41
|
|
23
|
+
deepwork/schemas/job_schema.py,sha256=R_82YmLWGn6lV1arjJ7_EzYFMYoTy1tzdUBk34mdNSY,9388
|
|
24
|
+
deepwork/schemas/rules_schema.py,sha256=CNv0_ambXTHZKFy81VCpew6KmHslSQPwLG7yd2x-z_0,4666
|
|
25
|
+
deepwork/standard_jobs/deepwork_jobs/AGENTS.md,sha256=Y6I4jZ8DfN0RFY3UF5bgQRZvL7wQD9P0lgE7EZM6CGI,2252
|
|
26
|
+
deepwork/standard_jobs/deepwork_jobs/job.yml,sha256=fNZVh_iW0sUXUCs_2YcIi2CVVLcnrM95YE9Zq4cZeNI,5399
|
|
27
|
+
deepwork/standard_jobs/deepwork_jobs/make_new_job.sh,sha256=JArfFU9lEaJPRsXRL3rU1IMt2p6Bq0s2C9f98aJ7Mxg,3878
|
|
28
|
+
deepwork/standard_jobs/deepwork_jobs/steps/define.md,sha256=j7S2ONQ-hih4ksIwge91yOD1SGa5VZcXw-J_RVhbFqg,13179
|
|
29
|
+
deepwork/standard_jobs/deepwork_jobs/steps/implement.md,sha256=Kgz6xBSFWNjKcTzRYOOyHox1cBl6SSmPbBaVPnKfbAY,10603
|
|
30
|
+
deepwork/standard_jobs/deepwork_jobs/steps/learn.md,sha256=YeWoMgEvpsjDyknMGjopRa4dnfRbyxCar3f4AMXi6K0,10179
|
|
31
|
+
deepwork/standard_jobs/deepwork_jobs/steps/supplemental_file_references.md,sha256=uKDEwB3TxMLK-Zim3QQfkvaW5W6AVWHjWnH25aY6wCw,1478
|
|
32
|
+
deepwork/standard_jobs/deepwork_jobs/templates/agents.md.template,sha256=SUJL862C6-DnT9lK3sNIZ5T2wVgXN4YRph4FrKtFnLo,739
|
|
33
|
+
deepwork/standard_jobs/deepwork_jobs/templates/job.yml.example,sha256=roRi6sIGFGmPCkoVW26HfuTBjAO8-pPsxI5-Gfg3rc0,2361
|
|
34
|
+
deepwork/standard_jobs/deepwork_jobs/templates/job.yml.template,sha256=3I-VQvqXVJNZ_Vb5Ik28JsrEbGKbyLTZcuKxdEmV5s0,1789
|
|
35
|
+
deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.example,sha256=HXcjVaQz2HsDiA4ClnIeLvysVOGrFJ_5Tr-pm6dhdwc,2706
|
|
36
|
+
deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.template,sha256=6n9jFFuda4r549Oo-LBPKixFD3NvDl5MwEg5V7ItQBg,1286
|
|
37
|
+
deepwork/standard_jobs/deepwork_rules/job.yml,sha256=N61yhHmDeN6gldcQ7FytENmNe-r0G-C72ziEVizz7Jo,2164
|
|
38
|
+
deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh,sha256=e1iPltUxA6giTLD5TcyjWAr_3JCb7wLwYIqE8WHqCYs,1287
|
|
39
|
+
deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml,sha256=uZkGMROaICKik6-Mfa1dM-8608Y9L-VrYG5YAbVN8yw,186
|
|
40
|
+
deepwork/standard_jobs/deepwork_rules/hooks/user_prompt_submit.sh,sha256=TxwYb7kBW-cfHmcQoruJBjCTWvdWbQVQIMplNgzMuOs,498
|
|
41
|
+
deepwork/standard_jobs/deepwork_rules/rules/.gitkeep,sha256=tOhdLTAbVXm_Z4Gl85recPNonTC0kFwzw6NKpeqTmH8,337
|
|
42
|
+
deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example,sha256=YaTuv-LseeqYSG6mk2LzORfyGJ0bGaXuTB8IJkwzrDI,260
|
|
43
|
+
deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example,sha256=2uZHS-LjZfL81i7COJvIA0Q5lqa-aWyLx060ouSnlrw,305
|
|
44
|
+
deepwork/standard_jobs/deepwork_rules/rules/security-review.md.example,sha256=gPAk5bWLqXm5mO6vlYAU7FGDQbNlXE-Fuf2mU4sru80,285
|
|
45
|
+
deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md,sha256=H-o8X92AerumY2nimLzX-PniN2FGWAPI4tNGMpEo7nM,1457
|
|
46
|
+
deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example,sha256=Uwg0l85nB69yY8MWwF46EUwx3UJ3qIHXb2ywxp_VNW0,320
|
|
47
|
+
deepwork/standard_jobs/deepwork_rules/steps/define.md,sha256=TP3j90bh0zsGQniAo2GkX9xbLtB_XkaA4IudcbAZ0YY,8349
|
|
48
|
+
deepwork/templates/__init__.py,sha256=APvjx_u7eRUerw9yA_fJ1ZqCzYA-FWUCV9HCz0RgjOc,50
|
|
49
|
+
deepwork/templates/claude/skill-job-meta.md.jinja,sha256=109cnDYFpfIJhW4VBu0lJulvmBubIhoUUZyYvpqeJdM,2021
|
|
50
|
+
deepwork/templates/claude/skill-job-step.md.jinja,sha256=ENsfW1PTBR5LwVbox3BMqnaUZRDNuvtOETxnpB8luCE,5957
|
|
51
|
+
deepwork/templates/gemini/skill-job-meta.toml.jinja,sha256=FOkRNP3pGbvwcwwt3k6WF_qsCIkj1AbLYtF3mlkqAsA,2157
|
|
52
|
+
deepwork/templates/gemini/skill-job-step.toml.jinja,sha256=RghMd_Yj0y2xmVdG5WOuk6bDV3UPVYaWS8W6qLYSalw,4262
|
|
53
|
+
deepwork/utils/__init__.py,sha256=AtvE49IFI8Rg36O4cNIlzB-oxvkW3apFgXExn8GSk6s,38
|
|
54
|
+
deepwork/utils/fs.py,sha256=94OUvUrqGebjHVtnjd5vXL6DalKNdpRu-iAPsHvAPjI,3499
|
|
55
|
+
deepwork/utils/git.py,sha256=J4tAB1zE6-WMAEHbarevhmSvvPLkeKBpiRv1UxUVwYk,3748
|
|
56
|
+
deepwork/utils/validation.py,sha256=SyFg9fIu1JCDMbssQgJRCTUNToDNcINccn8lje-tjts,851
|
|
57
|
+
deepwork/utils/yaml_utils.py,sha256=X8c9yEqxEgw5CdPQ23f1Wz8SSP783MMGKyGV_2SKaNU,2454
|
|
58
|
+
deepwork-0.3.1.dist-info/METADATA,sha256=kbBslaPiI2kmRcWmq0Pjty9Z2gyzU6hvoOqGWHrKj6k,12277
|
|
59
|
+
deepwork-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
60
|
+
deepwork-0.3.1.dist-info/entry_points.txt,sha256=RhJBySzm619kh-yIdsAyfFXInAlY8Jm-39FLIBcOj2s,51
|
|
61
|
+
deepwork-0.3.1.dist-info/licenses/LICENSE.md,sha256=W0EtJVYf0cQ_awukOCW1ETwNSpV2RKqnAGfoOjyz_K8,4126
|
|
62
|
+
deepwork-0.3.1.dist-info/RECORD,,
|
deepwork/core/policy_parser.py
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
"""Policy definition parser."""
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from fnmatch import fnmatch
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
import yaml
|
|
9
|
-
|
|
10
|
-
from deepwork.schemas.policy_schema import POLICY_SCHEMA
|
|
11
|
-
from deepwork.utils.validation import ValidationError, validate_against_schema
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class PolicyParseError(Exception):
|
|
15
|
-
"""Exception raised for policy parsing errors."""
|
|
16
|
-
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Valid compare_to values
|
|
21
|
-
COMPARE_TO_VALUES = frozenset({"base", "default_tip", "prompt"})
|
|
22
|
-
DEFAULT_COMPARE_TO = "base"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class Policy:
|
|
27
|
-
"""Represents a single policy definition."""
|
|
28
|
-
|
|
29
|
-
name: str
|
|
30
|
-
triggers: list[str] # Normalized to list
|
|
31
|
-
safety: list[str] = field(default_factory=list) # Normalized to list, empty if not specified
|
|
32
|
-
instructions: str = "" # Resolved content (either inline or from file)
|
|
33
|
-
compare_to: str = DEFAULT_COMPARE_TO # What to compare against: base, default_tip, or prompt
|
|
34
|
-
|
|
35
|
-
@classmethod
|
|
36
|
-
def from_dict(cls, data: dict[str, Any], base_dir: Path | None = None) -> "Policy":
|
|
37
|
-
"""
|
|
38
|
-
Create Policy from dictionary.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
data: Parsed YAML data for a single policy
|
|
42
|
-
base_dir: Base directory for resolving instructions_file paths
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
Policy instance
|
|
46
|
-
|
|
47
|
-
Raises:
|
|
48
|
-
PolicyParseError: If instructions cannot be resolved
|
|
49
|
-
"""
|
|
50
|
-
# Normalize trigger to list
|
|
51
|
-
trigger = data["trigger"]
|
|
52
|
-
triggers = [trigger] if isinstance(trigger, str) else list(trigger)
|
|
53
|
-
|
|
54
|
-
# Normalize safety to list (empty if not present)
|
|
55
|
-
safety_data = data.get("safety", [])
|
|
56
|
-
safety = [safety_data] if isinstance(safety_data, str) else list(safety_data)
|
|
57
|
-
|
|
58
|
-
# Resolve instructions
|
|
59
|
-
if "instructions" in data:
|
|
60
|
-
instructions = data["instructions"]
|
|
61
|
-
elif "instructions_file" in data:
|
|
62
|
-
if base_dir is None:
|
|
63
|
-
raise PolicyParseError(
|
|
64
|
-
f"Policy '{data['name']}' uses instructions_file but no base_dir provided"
|
|
65
|
-
)
|
|
66
|
-
instructions_path = base_dir / data["instructions_file"]
|
|
67
|
-
if not instructions_path.exists():
|
|
68
|
-
raise PolicyParseError(
|
|
69
|
-
f"Policy '{data['name']}' instructions file not found: {instructions_path}"
|
|
70
|
-
)
|
|
71
|
-
try:
|
|
72
|
-
instructions = instructions_path.read_text()
|
|
73
|
-
except Exception as e:
|
|
74
|
-
raise PolicyParseError(
|
|
75
|
-
f"Policy '{data['name']}' failed to read instructions file: {e}"
|
|
76
|
-
) from e
|
|
77
|
-
else:
|
|
78
|
-
# Schema should catch this, but be defensive
|
|
79
|
-
raise PolicyParseError(
|
|
80
|
-
f"Policy '{data['name']}' must have either 'instructions' or 'instructions_file'"
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
# Get compare_to (defaults to DEFAULT_COMPARE_TO)
|
|
84
|
-
compare_to = data.get("compare_to", DEFAULT_COMPARE_TO)
|
|
85
|
-
|
|
86
|
-
return cls(
|
|
87
|
-
name=data["name"],
|
|
88
|
-
triggers=triggers,
|
|
89
|
-
safety=safety,
|
|
90
|
-
instructions=instructions,
|
|
91
|
-
compare_to=compare_to,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def matches_pattern(file_path: str, patterns: list[str]) -> bool:
|
|
96
|
-
"""
|
|
97
|
-
Check if a file path matches any of the given glob patterns.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
file_path: File path to check (relative path)
|
|
101
|
-
patterns: List of glob patterns to match against
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
True if the file matches any pattern
|
|
105
|
-
"""
|
|
106
|
-
for pattern in patterns:
|
|
107
|
-
if _matches_glob(file_path, pattern):
|
|
108
|
-
return True
|
|
109
|
-
return False
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _matches_glob(file_path: str, pattern: str) -> bool:
|
|
113
|
-
"""
|
|
114
|
-
Match a file path against a glob pattern, supporting ** for recursive matching.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
file_path: File path to check
|
|
118
|
-
pattern: Glob pattern (supports *, **, ?)
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
True if matches
|
|
122
|
-
"""
|
|
123
|
-
# Normalize path separators
|
|
124
|
-
file_path = file_path.replace("\\", "/")
|
|
125
|
-
pattern = pattern.replace("\\", "/")
|
|
126
|
-
|
|
127
|
-
# Handle ** patterns (recursive directory matching)
|
|
128
|
-
if "**" in pattern:
|
|
129
|
-
# Split pattern by **
|
|
130
|
-
parts = pattern.split("**")
|
|
131
|
-
|
|
132
|
-
if len(parts) == 2:
|
|
133
|
-
prefix, suffix = parts[0], parts[1]
|
|
134
|
-
|
|
135
|
-
# Remove leading/trailing slashes from suffix
|
|
136
|
-
suffix = suffix.lstrip("/")
|
|
137
|
-
|
|
138
|
-
# Check if prefix matches the start of the path
|
|
139
|
-
if prefix:
|
|
140
|
-
prefix = prefix.rstrip("/")
|
|
141
|
-
if not file_path.startswith(prefix + "/") and file_path != prefix:
|
|
142
|
-
return False
|
|
143
|
-
# Get the remaining path after prefix
|
|
144
|
-
remaining = file_path[len(prefix) :].lstrip("/")
|
|
145
|
-
else:
|
|
146
|
-
remaining = file_path
|
|
147
|
-
|
|
148
|
-
# If no suffix, any remaining path matches
|
|
149
|
-
if not suffix:
|
|
150
|
-
return True
|
|
151
|
-
|
|
152
|
-
# Check if suffix matches the end of any remaining path segment
|
|
153
|
-
# For pattern "src/**/*.py", suffix is "*.py"
|
|
154
|
-
# We need to match *.py against the filename portion
|
|
155
|
-
remaining_parts = remaining.split("/")
|
|
156
|
-
for i in range(len(remaining_parts)):
|
|
157
|
-
test_path = "/".join(remaining_parts[i:])
|
|
158
|
-
if fnmatch(test_path, suffix):
|
|
159
|
-
return True
|
|
160
|
-
# Also try just the filename
|
|
161
|
-
if fnmatch(remaining_parts[-1], suffix):
|
|
162
|
-
return True
|
|
163
|
-
|
|
164
|
-
return False
|
|
165
|
-
|
|
166
|
-
# Simple pattern without **
|
|
167
|
-
return fnmatch(file_path, pattern)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def evaluate_policy(policy: Policy, changed_files: list[str]) -> bool:
|
|
171
|
-
"""
|
|
172
|
-
Evaluate whether a policy should fire based on changed files.
|
|
173
|
-
|
|
174
|
-
A policy fires if:
|
|
175
|
-
- At least one changed file matches a trigger pattern
|
|
176
|
-
- AND no changed file matches a safety pattern
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
policy: Policy to evaluate
|
|
180
|
-
changed_files: List of changed file paths (relative)
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
True if the policy should fire
|
|
184
|
-
"""
|
|
185
|
-
# Check if any trigger matches
|
|
186
|
-
trigger_matched = False
|
|
187
|
-
for file_path in changed_files:
|
|
188
|
-
if matches_pattern(file_path, policy.triggers):
|
|
189
|
-
trigger_matched = True
|
|
190
|
-
break
|
|
191
|
-
|
|
192
|
-
if not trigger_matched:
|
|
193
|
-
return False
|
|
194
|
-
|
|
195
|
-
# Check if any safety pattern matches
|
|
196
|
-
if policy.safety:
|
|
197
|
-
for file_path in changed_files:
|
|
198
|
-
if matches_pattern(file_path, policy.safety):
|
|
199
|
-
# Safety file was also changed, don't fire
|
|
200
|
-
return False
|
|
201
|
-
|
|
202
|
-
return True
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def evaluate_policies(
|
|
206
|
-
policies: list[Policy],
|
|
207
|
-
changed_files: list[str],
|
|
208
|
-
promised_policies: set[str] | None = None,
|
|
209
|
-
) -> list[Policy]:
|
|
210
|
-
"""
|
|
211
|
-
Evaluate which policies should fire.
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
policies: List of policies to evaluate
|
|
215
|
-
changed_files: List of changed file paths (relative)
|
|
216
|
-
promised_policies: Set of policy names that have been marked as addressed
|
|
217
|
-
via <promise> tags (these are skipped)
|
|
218
|
-
|
|
219
|
-
Returns:
|
|
220
|
-
List of policies that should fire (trigger matches, no safety match, not promised)
|
|
221
|
-
"""
|
|
222
|
-
if promised_policies is None:
|
|
223
|
-
promised_policies = set()
|
|
224
|
-
|
|
225
|
-
fired_policies = []
|
|
226
|
-
for policy in policies:
|
|
227
|
-
# Skip if already promised/addressed
|
|
228
|
-
if policy.name in promised_policies:
|
|
229
|
-
continue
|
|
230
|
-
|
|
231
|
-
if evaluate_policy(policy, changed_files):
|
|
232
|
-
fired_policies.append(policy)
|
|
233
|
-
|
|
234
|
-
return fired_policies
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def parse_policy_file(policy_path: Path | str, base_dir: Path | None = None) -> list[Policy]:
|
|
238
|
-
"""
|
|
239
|
-
Parse policy definitions from a YAML file.
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
policy_path: Path to .deepwork.policy.yml file
|
|
243
|
-
base_dir: Base directory for resolving instructions_file paths.
|
|
244
|
-
Defaults to the directory containing the policy file.
|
|
245
|
-
|
|
246
|
-
Returns:
|
|
247
|
-
List of parsed Policy objects
|
|
248
|
-
|
|
249
|
-
Raises:
|
|
250
|
-
PolicyParseError: If parsing fails or validation errors occur
|
|
251
|
-
"""
|
|
252
|
-
policy_path = Path(policy_path)
|
|
253
|
-
|
|
254
|
-
if not policy_path.exists():
|
|
255
|
-
raise PolicyParseError(f"Policy file does not exist: {policy_path}")
|
|
256
|
-
|
|
257
|
-
if not policy_path.is_file():
|
|
258
|
-
raise PolicyParseError(f"Policy path is not a file: {policy_path}")
|
|
259
|
-
|
|
260
|
-
# Default base_dir to policy file's directory
|
|
261
|
-
if base_dir is None:
|
|
262
|
-
base_dir = policy_path.parent
|
|
263
|
-
|
|
264
|
-
# Load YAML (policies are stored as a list, not a dict)
|
|
265
|
-
try:
|
|
266
|
-
with open(policy_path, encoding="utf-8") as f:
|
|
267
|
-
policy_data = yaml.safe_load(f)
|
|
268
|
-
except yaml.YAMLError as e:
|
|
269
|
-
raise PolicyParseError(f"Failed to parse policy YAML: {e}") from e
|
|
270
|
-
except OSError as e:
|
|
271
|
-
raise PolicyParseError(f"Failed to read policy file: {e}") from e
|
|
272
|
-
|
|
273
|
-
# Handle empty file or null content
|
|
274
|
-
if policy_data is None:
|
|
275
|
-
return []
|
|
276
|
-
|
|
277
|
-
# Validate it's a list (schema expects array)
|
|
278
|
-
if not isinstance(policy_data, list):
|
|
279
|
-
raise PolicyParseError(
|
|
280
|
-
f"Policy file must contain a list of policies, got {type(policy_data).__name__}"
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
# Validate against schema
|
|
284
|
-
try:
|
|
285
|
-
validate_against_schema(policy_data, POLICY_SCHEMA)
|
|
286
|
-
except ValidationError as e:
|
|
287
|
-
raise PolicyParseError(f"Policy definition validation failed: {e}") from e
|
|
288
|
-
|
|
289
|
-
# Parse into dataclasses
|
|
290
|
-
policies = []
|
|
291
|
-
for policy_item in policy_data:
|
|
292
|
-
policy = Policy.from_dict(policy_item, base_dir)
|
|
293
|
-
policies.append(policy)
|
|
294
|
-
|
|
295
|
-
return policies
|