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
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Policy evaluation module for DeepWork hooks.
|
|
3
|
-
|
|
4
|
-
This module is called by the policy_stop_hook.sh script to evaluate which policies
|
|
5
|
-
should fire based on changed files and conversation context.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
python -m deepwork.hooks.evaluate_policies \
|
|
9
|
-
--policy-file .deepwork.policy.yml
|
|
10
|
-
|
|
11
|
-
The conversation context is read from stdin and checked for <promise> tags
|
|
12
|
-
that indicate policies have already been addressed.
|
|
13
|
-
|
|
14
|
-
Changed files are computed based on each policy's compare_to setting:
|
|
15
|
-
- base: Compare to merge-base with default branch (default)
|
|
16
|
-
- default_tip: Two-dot diff against default branch tip
|
|
17
|
-
- prompt: Compare to state captured at prompt submission
|
|
18
|
-
|
|
19
|
-
Output is JSON suitable for Claude Code Stop hooks:
|
|
20
|
-
{"decision": "block", "reason": "..."} # Block stop, policies need attention
|
|
21
|
-
{} # No policies fired, allow stop
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
import argparse
|
|
25
|
-
import json
|
|
26
|
-
import re
|
|
27
|
-
import subprocess
|
|
28
|
-
import sys
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
|
|
31
|
-
from deepwork.core.policy_parser import (
|
|
32
|
-
Policy,
|
|
33
|
-
PolicyParseError,
|
|
34
|
-
evaluate_policy,
|
|
35
|
-
parse_policy_file,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_default_branch() -> str:
|
|
40
|
-
"""
|
|
41
|
-
Get the default branch name (main or master).
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
Default branch name, or "main" if cannot be determined.
|
|
45
|
-
"""
|
|
46
|
-
# Try to get the default branch from remote HEAD
|
|
47
|
-
try:
|
|
48
|
-
result = subprocess.run(
|
|
49
|
-
["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
50
|
-
capture_output=True,
|
|
51
|
-
text=True,
|
|
52
|
-
check=True,
|
|
53
|
-
)
|
|
54
|
-
# Output is like "refs/remotes/origin/main"
|
|
55
|
-
return result.stdout.strip().split("/")[-1]
|
|
56
|
-
except subprocess.CalledProcessError:
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
# Try common default branch names
|
|
60
|
-
for branch in ["main", "master"]:
|
|
61
|
-
try:
|
|
62
|
-
subprocess.run(
|
|
63
|
-
["git", "rev-parse", "--verify", f"origin/{branch}"],
|
|
64
|
-
capture_output=True,
|
|
65
|
-
check=True,
|
|
66
|
-
)
|
|
67
|
-
return branch
|
|
68
|
-
except subprocess.CalledProcessError:
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
# Fall back to main
|
|
72
|
-
return "main"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_changed_files_base() -> list[str]:
|
|
76
|
-
"""
|
|
77
|
-
Get files changed relative to the base of the current branch.
|
|
78
|
-
|
|
79
|
-
This finds the merge-base between the current branch and the default branch,
|
|
80
|
-
then returns all files changed since that point.
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
List of changed file paths.
|
|
84
|
-
"""
|
|
85
|
-
default_branch = get_default_branch()
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
# Get the merge-base (where current branch diverged from default)
|
|
89
|
-
result = subprocess.run(
|
|
90
|
-
["git", "merge-base", "HEAD", f"origin/{default_branch}"],
|
|
91
|
-
capture_output=True,
|
|
92
|
-
text=True,
|
|
93
|
-
check=True,
|
|
94
|
-
)
|
|
95
|
-
merge_base = result.stdout.strip()
|
|
96
|
-
|
|
97
|
-
# Stage all changes so they appear in diff
|
|
98
|
-
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
99
|
-
|
|
100
|
-
# Get files changed since merge-base (including staged)
|
|
101
|
-
result = subprocess.run(
|
|
102
|
-
["git", "diff", "--name-only", merge_base, "HEAD"],
|
|
103
|
-
capture_output=True,
|
|
104
|
-
text=True,
|
|
105
|
-
check=True,
|
|
106
|
-
)
|
|
107
|
-
committed_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
108
|
-
|
|
109
|
-
# Also get staged changes not yet committed
|
|
110
|
-
result = subprocess.run(
|
|
111
|
-
["git", "diff", "--name-only", "--cached"],
|
|
112
|
-
capture_output=True,
|
|
113
|
-
text=True,
|
|
114
|
-
check=False,
|
|
115
|
-
)
|
|
116
|
-
staged_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
117
|
-
|
|
118
|
-
# Get untracked files
|
|
119
|
-
result = subprocess.run(
|
|
120
|
-
["git", "ls-files", "--others", "--exclude-standard"],
|
|
121
|
-
capture_output=True,
|
|
122
|
-
text=True,
|
|
123
|
-
check=False,
|
|
124
|
-
)
|
|
125
|
-
untracked_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
126
|
-
|
|
127
|
-
all_files = committed_files | staged_files | untracked_files
|
|
128
|
-
return sorted([f for f in all_files if f])
|
|
129
|
-
|
|
130
|
-
except subprocess.CalledProcessError:
|
|
131
|
-
return []
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def get_changed_files_default_tip() -> list[str]:
|
|
135
|
-
"""
|
|
136
|
-
Get files changed compared to the tip of the default branch.
|
|
137
|
-
|
|
138
|
-
This does a two-dot diff: what's different between HEAD and origin/default.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
List of changed file paths.
|
|
142
|
-
"""
|
|
143
|
-
default_branch = get_default_branch()
|
|
144
|
-
|
|
145
|
-
try:
|
|
146
|
-
# Stage all changes so they appear in diff
|
|
147
|
-
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
148
|
-
|
|
149
|
-
# Two-dot diff against default branch tip
|
|
150
|
-
result = subprocess.run(
|
|
151
|
-
["git", "diff", "--name-only", f"origin/{default_branch}..HEAD"],
|
|
152
|
-
capture_output=True,
|
|
153
|
-
text=True,
|
|
154
|
-
check=True,
|
|
155
|
-
)
|
|
156
|
-
committed_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
157
|
-
|
|
158
|
-
# Also get staged changes not yet committed
|
|
159
|
-
result = subprocess.run(
|
|
160
|
-
["git", "diff", "--name-only", "--cached"],
|
|
161
|
-
capture_output=True,
|
|
162
|
-
text=True,
|
|
163
|
-
check=False,
|
|
164
|
-
)
|
|
165
|
-
staged_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
166
|
-
|
|
167
|
-
# Get untracked files
|
|
168
|
-
result = subprocess.run(
|
|
169
|
-
["git", "ls-files", "--others", "--exclude-standard"],
|
|
170
|
-
capture_output=True,
|
|
171
|
-
text=True,
|
|
172
|
-
check=False,
|
|
173
|
-
)
|
|
174
|
-
untracked_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
175
|
-
|
|
176
|
-
all_files = committed_files | staged_files | untracked_files
|
|
177
|
-
return sorted([f for f in all_files if f])
|
|
178
|
-
|
|
179
|
-
except subprocess.CalledProcessError:
|
|
180
|
-
return []
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def get_changed_files_prompt() -> list[str]:
|
|
184
|
-
"""
|
|
185
|
-
Get files changed since the prompt was submitted.
|
|
186
|
-
|
|
187
|
-
This compares against the baseline captured by capture_prompt_work_tree.sh.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
List of changed file paths.
|
|
191
|
-
"""
|
|
192
|
-
baseline_path = Path(".deepwork/.last_work_tree")
|
|
193
|
-
|
|
194
|
-
try:
|
|
195
|
-
# Stage all changes so we can see them with --cached
|
|
196
|
-
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
197
|
-
|
|
198
|
-
# Get all staged files (includes what was just staged)
|
|
199
|
-
result = subprocess.run(
|
|
200
|
-
["git", "diff", "--name-only", "--cached"],
|
|
201
|
-
capture_output=True,
|
|
202
|
-
text=True,
|
|
203
|
-
check=False,
|
|
204
|
-
)
|
|
205
|
-
current_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
206
|
-
current_files = {f for f in current_files if f}
|
|
207
|
-
|
|
208
|
-
if baseline_path.exists():
|
|
209
|
-
# Read baseline and find new files
|
|
210
|
-
baseline_files = set(baseline_path.read_text().strip().split("\n"))
|
|
211
|
-
baseline_files = {f for f in baseline_files if f}
|
|
212
|
-
# Return files that are in current but not in baseline
|
|
213
|
-
new_files = current_files - baseline_files
|
|
214
|
-
return sorted(new_files)
|
|
215
|
-
else:
|
|
216
|
-
# No baseline, return all current changes
|
|
217
|
-
return sorted(current_files)
|
|
218
|
-
|
|
219
|
-
except (subprocess.CalledProcessError, OSError):
|
|
220
|
-
return []
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def get_changed_files_for_mode(mode: str) -> list[str]:
|
|
224
|
-
"""
|
|
225
|
-
Get changed files for a specific compare_to mode.
|
|
226
|
-
|
|
227
|
-
Args:
|
|
228
|
-
mode: One of 'base', 'default_tip', or 'prompt'
|
|
229
|
-
|
|
230
|
-
Returns:
|
|
231
|
-
List of changed file paths.
|
|
232
|
-
"""
|
|
233
|
-
if mode == "base":
|
|
234
|
-
return get_changed_files_base()
|
|
235
|
-
elif mode == "default_tip":
|
|
236
|
-
return get_changed_files_default_tip()
|
|
237
|
-
elif mode == "prompt":
|
|
238
|
-
return get_changed_files_prompt()
|
|
239
|
-
else:
|
|
240
|
-
# Unknown mode, fall back to base
|
|
241
|
-
return get_changed_files_base()
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def extract_promise_tags(text: str) -> set[str]:
|
|
245
|
-
"""
|
|
246
|
-
Extract policy names from <promise> tags in text.
|
|
247
|
-
|
|
248
|
-
Supported format:
|
|
249
|
-
- <promise>✓ Policy Name</promise>
|
|
250
|
-
|
|
251
|
-
Args:
|
|
252
|
-
text: Text to search for promise tags
|
|
253
|
-
|
|
254
|
-
Returns:
|
|
255
|
-
Set of policy names that have been promised/addressed
|
|
256
|
-
"""
|
|
257
|
-
# Match <promise>✓ Policy Name</promise> and extract the policy name
|
|
258
|
-
pattern = r"<promise>✓\s*([^<]+)</promise>"
|
|
259
|
-
matches = re.findall(pattern, text, re.IGNORECASE | re.DOTALL)
|
|
260
|
-
return {m.strip() for m in matches}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def format_policy_message(policies: list) -> str:
|
|
264
|
-
"""
|
|
265
|
-
Format triggered policies into a message for the agent.
|
|
266
|
-
|
|
267
|
-
Args:
|
|
268
|
-
policies: List of Policy objects that fired
|
|
269
|
-
|
|
270
|
-
Returns:
|
|
271
|
-
Formatted message with all policy instructions
|
|
272
|
-
"""
|
|
273
|
-
lines = ["## DeepWork Policies Triggered", ""]
|
|
274
|
-
lines.append(
|
|
275
|
-
"Comply with the following policies. "
|
|
276
|
-
"To mark a policy as addressed, include `<promise>✓ Policy Name</promise>` "
|
|
277
|
-
"in your response (replace Policy Name with the actual policy name)."
|
|
278
|
-
)
|
|
279
|
-
lines.append("")
|
|
280
|
-
|
|
281
|
-
for policy in policies:
|
|
282
|
-
lines.append(f"### Policy: {policy.name}")
|
|
283
|
-
lines.append("")
|
|
284
|
-
lines.append(policy.instructions.strip())
|
|
285
|
-
lines.append("")
|
|
286
|
-
|
|
287
|
-
return "\n".join(lines)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def main() -> None:
|
|
291
|
-
"""Main entry point for policy evaluation CLI."""
|
|
292
|
-
parser = argparse.ArgumentParser(
|
|
293
|
-
description="Evaluate DeepWork policies based on changed files"
|
|
294
|
-
)
|
|
295
|
-
parser.add_argument(
|
|
296
|
-
"--policy-file",
|
|
297
|
-
type=str,
|
|
298
|
-
required=True,
|
|
299
|
-
help="Path to .deepwork.policy.yml file",
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
args = parser.parse_args()
|
|
303
|
-
|
|
304
|
-
# Check if policy file exists
|
|
305
|
-
policy_path = Path(args.policy_file)
|
|
306
|
-
if not policy_path.exists():
|
|
307
|
-
# No policy file, nothing to evaluate
|
|
308
|
-
print("{}")
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
# Read conversation context from stdin (if available)
|
|
312
|
-
conversation_context = ""
|
|
313
|
-
if not sys.stdin.isatty():
|
|
314
|
-
try:
|
|
315
|
-
conversation_context = sys.stdin.read()
|
|
316
|
-
except Exception:
|
|
317
|
-
pass
|
|
318
|
-
|
|
319
|
-
# Extract promise tags from conversation
|
|
320
|
-
promised_policies = extract_promise_tags(conversation_context)
|
|
321
|
-
|
|
322
|
-
# Parse policies
|
|
323
|
-
try:
|
|
324
|
-
policies = parse_policy_file(policy_path)
|
|
325
|
-
except PolicyParseError as e:
|
|
326
|
-
# Log error to stderr, return empty result
|
|
327
|
-
print(f"Error parsing policy file: {e}", file=sys.stderr)
|
|
328
|
-
print("{}")
|
|
329
|
-
return
|
|
330
|
-
|
|
331
|
-
if not policies:
|
|
332
|
-
# No policies defined
|
|
333
|
-
print("{}")
|
|
334
|
-
return
|
|
335
|
-
|
|
336
|
-
# Group policies by compare_to mode to minimize git calls
|
|
337
|
-
policies_by_mode: dict[str, list[Policy]] = {}
|
|
338
|
-
for policy in policies:
|
|
339
|
-
mode = policy.compare_to
|
|
340
|
-
if mode not in policies_by_mode:
|
|
341
|
-
policies_by_mode[mode] = []
|
|
342
|
-
policies_by_mode[mode].append(policy)
|
|
343
|
-
|
|
344
|
-
# Get changed files for each mode and evaluate policies
|
|
345
|
-
fired_policies: list[Policy] = []
|
|
346
|
-
for mode, mode_policies in policies_by_mode.items():
|
|
347
|
-
changed_files = get_changed_files_for_mode(mode)
|
|
348
|
-
if not changed_files:
|
|
349
|
-
continue
|
|
350
|
-
|
|
351
|
-
for policy in mode_policies:
|
|
352
|
-
# Skip if already promised
|
|
353
|
-
if policy.name in promised_policies:
|
|
354
|
-
continue
|
|
355
|
-
# Evaluate this policy
|
|
356
|
-
if evaluate_policy(policy, changed_files):
|
|
357
|
-
fired_policies.append(policy)
|
|
358
|
-
|
|
359
|
-
if not fired_policies:
|
|
360
|
-
# No policies fired
|
|
361
|
-
print("{}")
|
|
362
|
-
return
|
|
363
|
-
|
|
364
|
-
# Format output for Claude Code Stop hooks
|
|
365
|
-
# Use "decision": "block" to prevent Claude from stopping
|
|
366
|
-
message = format_policy_message(fired_policies)
|
|
367
|
-
result = {
|
|
368
|
-
"decision": "block",
|
|
369
|
-
"reason": message,
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
print(json.dumps(result))
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if __name__ == "__main__":
|
|
376
|
-
main()
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"""JSON Schema definition for policy definitions."""
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
# JSON Schema for .deepwork.policy.yml files
|
|
6
|
-
# Policies are defined as an array of policy objects
|
|
7
|
-
POLICY_SCHEMA: dict[str, Any] = {
|
|
8
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
9
|
-
"type": "array",
|
|
10
|
-
"description": "List of policies that trigger based on file changes",
|
|
11
|
-
"items": {
|
|
12
|
-
"type": "object",
|
|
13
|
-
"required": ["name", "trigger"],
|
|
14
|
-
"properties": {
|
|
15
|
-
"name": {
|
|
16
|
-
"type": "string",
|
|
17
|
-
"minLength": 1,
|
|
18
|
-
"description": "Friendly name for the policy",
|
|
19
|
-
},
|
|
20
|
-
"trigger": {
|
|
21
|
-
"oneOf": [
|
|
22
|
-
{
|
|
23
|
-
"type": "string",
|
|
24
|
-
"minLength": 1,
|
|
25
|
-
"description": "Glob pattern for files that trigger this policy",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"type": "array",
|
|
29
|
-
"items": {"type": "string", "minLength": 1},
|
|
30
|
-
"minItems": 1,
|
|
31
|
-
"description": "List of glob patterns for files that trigger this policy",
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
"description": "Glob pattern(s) for files that, if changed, should trigger this policy",
|
|
35
|
-
},
|
|
36
|
-
"safety": {
|
|
37
|
-
"oneOf": [
|
|
38
|
-
{
|
|
39
|
-
"type": "string",
|
|
40
|
-
"minLength": 1,
|
|
41
|
-
"description": "Glob pattern for safety files",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"type": "array",
|
|
45
|
-
"items": {"type": "string", "minLength": 1},
|
|
46
|
-
"description": "List of glob patterns for safety files",
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
"description": "Glob pattern(s) for files that, if also changed, mean the policy doesn't need to trigger",
|
|
50
|
-
},
|
|
51
|
-
"instructions": {
|
|
52
|
-
"type": "string",
|
|
53
|
-
"minLength": 1,
|
|
54
|
-
"description": "Instructions to give the agent when this policy triggers",
|
|
55
|
-
},
|
|
56
|
-
"instructions_file": {
|
|
57
|
-
"type": "string",
|
|
58
|
-
"minLength": 1,
|
|
59
|
-
"description": "Path to a file containing instructions (alternative to inline instructions)",
|
|
60
|
-
},
|
|
61
|
-
"compare_to": {
|
|
62
|
-
"type": "string",
|
|
63
|
-
"enum": ["base", "default_tip", "prompt"],
|
|
64
|
-
"description": (
|
|
65
|
-
"What to compare against when detecting changed files. "
|
|
66
|
-
"'base' (default) compares to the base of the current branch. "
|
|
67
|
-
"'default_tip' compares to the tip of the default branch. "
|
|
68
|
-
"'prompt' compares to the state at the start of the prompt."
|
|
69
|
-
),
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
"oneOf": [
|
|
73
|
-
{"required": ["instructions"]},
|
|
74
|
-
{"required": ["instructions_file"]},
|
|
75
|
-
],
|
|
76
|
-
"additionalProperties": False,
|
|
77
|
-
},
|
|
78
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# capture_prompt_work_tree.sh - Captures the git work tree state at prompt submission
|
|
3
|
-
#
|
|
4
|
-
# This script creates a snapshot of the current git state by recording
|
|
5
|
-
# all files that have been modified, added, or deleted. This baseline
|
|
6
|
-
# is used for policies with compare_to: prompt to detect what changed
|
|
7
|
-
# during an agent response (between user prompts).
|
|
8
|
-
|
|
9
|
-
set -e
|
|
10
|
-
|
|
11
|
-
# Ensure .deepwork directory exists
|
|
12
|
-
mkdir -p .deepwork
|
|
13
|
-
|
|
14
|
-
# Stage all changes so we can diff against HEAD
|
|
15
|
-
git add -A 2>/dev/null || true
|
|
16
|
-
|
|
17
|
-
# Save the current state of changed files
|
|
18
|
-
# Using git diff --name-only HEAD to get the list of all changed files
|
|
19
|
-
git diff --name-only HEAD > .deepwork/.last_work_tree 2>/dev/null || true
|
|
20
|
-
|
|
21
|
-
# Also include untracked files not yet in the index
|
|
22
|
-
git ls-files --others --exclude-standard >> .deepwork/.last_work_tree 2>/dev/null || true
|
|
23
|
-
|
|
24
|
-
# Sort and deduplicate
|
|
25
|
-
if [ -f .deepwork/.last_work_tree ]; then
|
|
26
|
-
sort -u .deepwork/.last_work_tree -o .deepwork/.last_work_tree
|
|
27
|
-
fi
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# policy_stop_hook.sh - Evaluates policies when the agent stops
|
|
3
|
-
#
|
|
4
|
-
# This script is called as a Claude Code Stop hook. It:
|
|
5
|
-
# 1. Evaluates policies from .deepwork.policy.yml
|
|
6
|
-
# 2. Computes changed files based on each policy's compare_to setting
|
|
7
|
-
# 3. Checks for <promise> tags in the conversation transcript
|
|
8
|
-
# 4. Returns JSON to block stop if policies need attention
|
|
9
|
-
|
|
10
|
-
set -e
|
|
11
|
-
|
|
12
|
-
# Check if policy file exists
|
|
13
|
-
if [ ! -f .deepwork.policy.yml ]; then
|
|
14
|
-
# No policies defined, nothing to do
|
|
15
|
-
exit 0
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
# Read the hook input JSON from stdin
|
|
19
|
-
HOOK_INPUT=""
|
|
20
|
-
if [ ! -t 0 ]; then
|
|
21
|
-
HOOK_INPUT=$(cat)
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
# Extract transcript_path from the hook input JSON using jq
|
|
25
|
-
# Claude Code passes: {"session_id": "...", "transcript_path": "...", ...}
|
|
26
|
-
TRANSCRIPT_PATH=""
|
|
27
|
-
if [ -n "${HOOK_INPUT}" ]; then
|
|
28
|
-
TRANSCRIPT_PATH=$(echo "${HOOK_INPUT}" | jq -r '.transcript_path // empty' 2>/dev/null || echo "")
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# Extract conversation text from the JSONL transcript
|
|
32
|
-
# The transcript is JSONL format - each line is a JSON object
|
|
33
|
-
# We need to extract the text content from assistant messages
|
|
34
|
-
conversation_context=""
|
|
35
|
-
if [ -n "${TRANSCRIPT_PATH}" ] && [ -f "${TRANSCRIPT_PATH}" ]; then
|
|
36
|
-
# Extract text content from all assistant messages in the transcript
|
|
37
|
-
# Each line is a JSON object; we extract .message.content[].text for assistant messages
|
|
38
|
-
conversation_context=$(cat "${TRANSCRIPT_PATH}" | \
|
|
39
|
-
grep -E '"role"\s*:\s*"assistant"' | \
|
|
40
|
-
jq -r '.message.content // [] | map(select(.type == "text")) | map(.text) | join("\n")' 2>/dev/null | \
|
|
41
|
-
tr -d '\0' || echo "")
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# Call the Python evaluator
|
|
45
|
-
# The Python module handles:
|
|
46
|
-
# - Parsing the policy file
|
|
47
|
-
# - Computing changed files based on each policy's compare_to setting
|
|
48
|
-
# - Matching changed files against triggers/safety patterns
|
|
49
|
-
# - Checking for promise tags in the conversation context
|
|
50
|
-
# - Generating appropriate JSON output
|
|
51
|
-
result=$(echo "${conversation_context}" | python -m deepwork.hooks.evaluate_policies \
|
|
52
|
-
--policy-file .deepwork.policy.yml \
|
|
53
|
-
2>/dev/null || echo '{}')
|
|
54
|
-
|
|
55
|
-
# Output the result (JSON for Claude Code hooks)
|
|
56
|
-
echo "${result}"
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
name: deepwork_policy
|
|
2
|
-
version: "0.1.0"
|
|
3
|
-
summary: "Policy enforcement for AI agent sessions"
|
|
4
|
-
description: |
|
|
5
|
-
Manages policies that automatically trigger when certain files change during an AI agent session.
|
|
6
|
-
Policies help ensure that code changes follow team guidelines, documentation is updated,
|
|
7
|
-
and architectural decisions are respected.
|
|
8
|
-
|
|
9
|
-
Policies are defined in a `.deepwork.policy.yml` file at the root of your project. Each policy
|
|
10
|
-
specifies:
|
|
11
|
-
- Trigger patterns: Glob patterns for files that, when changed, should trigger the policy
|
|
12
|
-
- Safety patterns: Glob patterns for files that, if also changed, mean the policy doesn't need to fire
|
|
13
|
-
- Instructions: What the agent should do when the policy triggers
|
|
14
|
-
|
|
15
|
-
Example use cases:
|
|
16
|
-
- Update installation docs when configuration files change
|
|
17
|
-
- Require security review when authentication code is modified
|
|
18
|
-
- Ensure API documentation stays in sync with API code
|
|
19
|
-
- Remind developers to update changelogs
|
|
20
|
-
|
|
21
|
-
changelog:
|
|
22
|
-
- version: "0.1.0"
|
|
23
|
-
changes: "Initial version"
|
|
24
|
-
|
|
25
|
-
steps:
|
|
26
|
-
- id: define
|
|
27
|
-
name: "Define Policy"
|
|
28
|
-
description: "Create or update policy entries in .deepwork.policy.yml"
|
|
29
|
-
instructions_file: steps/define.md
|
|
30
|
-
inputs:
|
|
31
|
-
- name: policy_purpose
|
|
32
|
-
description: "What guideline or constraint should this policy enforce?"
|
|
33
|
-
outputs:
|
|
34
|
-
- .deepwork.policy.yml
|
|
35
|
-
dependencies: []
|