deepwork 0.3.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 +19 -15
- deepwork/core/rules_parser.py +58 -10
- deepwork/hooks/rules_check.py +198 -12
- deepwork/schemas/rules_schema.py +38 -6
- deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +14 -11
- deepwork/standard_jobs/deepwork_rules/job.yml +8 -0
- deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md +1 -0
- {deepwork-0.3.0.dist-info → deepwork-0.3.1.dist-info}/METADATA +3 -1
- {deepwork-0.3.0.dist-info → deepwork-0.3.1.dist-info}/RECORD +12 -12
- {deepwork-0.3.0.dist-info → deepwork-0.3.1.dist-info}/WHEEL +0 -0
- {deepwork-0.3.0.dist-info → deepwork-0.3.1.dist-info}/entry_points.txt +0 -0
- {deepwork-0.3.0.dist-info → deepwork-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
deepwork/cli/install.py
CHANGED
|
@@ -283,21 +283,25 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
|
|
|
283
283
|
available_adapters = detector.detect_all_platforms()
|
|
284
284
|
|
|
285
285
|
if not available_adapters:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
286
|
+
# No platforms detected - default to Claude Code
|
|
287
|
+
console.print(" [dim]•[/dim] No AI platform detected, defaulting to Claude Code")
|
|
288
|
+
|
|
289
|
+
# Create .claude directory
|
|
290
|
+
claude_dir = project_path / ".claude"
|
|
291
|
+
ensure_dir(claude_dir)
|
|
292
|
+
console.print(f" [green]✓[/green] Created {claude_dir.relative_to(project_path)}/")
|
|
293
|
+
|
|
294
|
+
# Get Claude adapter
|
|
295
|
+
claude_adapter_class = AgentAdapter.get("claude")
|
|
296
|
+
claude_adapter = claude_adapter_class(project_root=project_path)
|
|
297
|
+
platforms_to_add = [claude_adapter.name]
|
|
298
|
+
detected_adapters = [claude_adapter]
|
|
299
|
+
else:
|
|
300
|
+
# Add all detected platforms
|
|
301
|
+
for adapter in available_adapters:
|
|
302
|
+
console.print(f" [green]✓[/green] {adapter.display_name} detected")
|
|
303
|
+
platforms_to_add.append(adapter.name)
|
|
304
|
+
detected_adapters = available_adapters
|
|
301
305
|
|
|
302
306
|
# Step 3: Create .deepwork/ directory structure
|
|
303
307
|
console.print("[yellow]→[/yellow] Creating DeepWork directory structure...")
|
deepwork/core/rules_parser.py
CHANGED
|
@@ -29,6 +29,7 @@ class DetectionMode(Enum):
|
|
|
29
29
|
TRIGGER_SAFETY = "trigger_safety" # Fire when trigger matches, safety doesn't
|
|
30
30
|
SET = "set" # Bidirectional file correspondence
|
|
31
31
|
PAIR = "pair" # Directional file correspondence
|
|
32
|
+
CREATED = "created" # Fire when created files match patterns
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class ActionType(Enum):
|
|
@@ -40,7 +41,6 @@ class ActionType(Enum):
|
|
|
40
41
|
|
|
41
42
|
# Valid compare_to values
|
|
42
43
|
COMPARE_TO_VALUES = frozenset({"base", "default_tip", "prompt"})
|
|
43
|
-
DEFAULT_COMPARE_TO = "base"
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
@@ -69,19 +69,22 @@ class Rule:
|
|
|
69
69
|
|
|
70
70
|
# Detection mode (exactly one must be set)
|
|
71
71
|
detection_mode: DetectionMode
|
|
72
|
+
|
|
73
|
+
# Common options (required)
|
|
74
|
+
compare_to: str # Required: "base", "default_tip", or "prompt"
|
|
75
|
+
|
|
76
|
+
# Detection mode details (optional, depends on mode)
|
|
72
77
|
triggers: list[str] = field(default_factory=list) # For TRIGGER_SAFETY mode
|
|
73
78
|
safety: list[str] = field(default_factory=list) # For TRIGGER_SAFETY mode
|
|
74
79
|
set_patterns: list[str] = field(default_factory=list) # For SET mode
|
|
75
80
|
pair_config: PairConfig | None = None # For PAIR mode
|
|
81
|
+
created_patterns: list[str] = field(default_factory=list) # For CREATED mode
|
|
76
82
|
|
|
77
83
|
# Action type
|
|
78
84
|
action_type: ActionType = ActionType.PROMPT
|
|
79
85
|
instructions: str = "" # For PROMPT action (markdown body)
|
|
80
86
|
command_action: CommandAction | None = None # For COMMAND action
|
|
81
87
|
|
|
82
|
-
# Common options
|
|
83
|
-
compare_to: str = DEFAULT_COMPARE_TO
|
|
84
|
-
|
|
85
88
|
@classmethod
|
|
86
89
|
def from_frontmatter(
|
|
87
90
|
cls,
|
|
@@ -112,10 +115,11 @@ class Rule:
|
|
|
112
115
|
has_trigger = "trigger" in frontmatter
|
|
113
116
|
has_set = "set" in frontmatter
|
|
114
117
|
has_pair = "pair" in frontmatter
|
|
118
|
+
has_created = "created" in frontmatter
|
|
115
119
|
|
|
116
|
-
mode_count = sum([has_trigger, has_set, has_pair])
|
|
120
|
+
mode_count = sum([has_trigger, has_set, has_pair, has_created])
|
|
117
121
|
if mode_count == 0:
|
|
118
|
-
raise RulesParseError(f"Rule '{name}' must have 'trigger', 'set', or '
|
|
122
|
+
raise RulesParseError(f"Rule '{name}' must have 'trigger', 'set', 'pair', or 'created'")
|
|
119
123
|
if mode_count > 1:
|
|
120
124
|
raise RulesParseError(f"Rule '{name}' has multiple detection modes - use only one")
|
|
121
125
|
|
|
@@ -125,6 +129,7 @@ class Rule:
|
|
|
125
129
|
safety: list[str] = []
|
|
126
130
|
set_patterns: list[str] = []
|
|
127
131
|
pair_config: PairConfig | None = None
|
|
132
|
+
created_patterns: list[str] = []
|
|
128
133
|
|
|
129
134
|
if has_trigger:
|
|
130
135
|
detection_mode = DetectionMode.TRIGGER_SAFETY
|
|
@@ -149,6 +154,11 @@ class Rule:
|
|
|
149
154
|
expects=expects_list,
|
|
150
155
|
)
|
|
151
156
|
|
|
157
|
+
elif has_created:
|
|
158
|
+
detection_mode = DetectionMode.CREATED
|
|
159
|
+
created = frontmatter["created"]
|
|
160
|
+
created_patterns = [created] if isinstance(created, str) else list(created)
|
|
161
|
+
|
|
152
162
|
# Determine action type
|
|
153
163
|
action_type: ActionType
|
|
154
164
|
command_action: CommandAction | None = None
|
|
@@ -166,8 +176,8 @@ class Rule:
|
|
|
166
176
|
if not markdown_body.strip():
|
|
167
177
|
raise RulesParseError(f"Rule '{name}' with prompt action requires markdown body")
|
|
168
178
|
|
|
169
|
-
# Get compare_to
|
|
170
|
-
compare_to = frontmatter
|
|
179
|
+
# Get compare_to (required field)
|
|
180
|
+
compare_to = frontmatter["compare_to"]
|
|
171
181
|
|
|
172
182
|
return cls(
|
|
173
183
|
name=name,
|
|
@@ -177,6 +187,7 @@ class Rule:
|
|
|
177
187
|
safety=safety,
|
|
178
188
|
set_patterns=set_patterns,
|
|
179
189
|
pair_config=pair_config,
|
|
190
|
+
created_patterns=created_patterns,
|
|
180
191
|
action_type=action_type,
|
|
181
192
|
instructions=markdown_body.strip(),
|
|
182
193
|
command_action=command_action,
|
|
@@ -418,6 +429,22 @@ def evaluate_pair_correspondence(
|
|
|
418
429
|
return should_fire, trigger_files, missing_files
|
|
419
430
|
|
|
420
431
|
|
|
432
|
+
def evaluate_created(
|
|
433
|
+
rule: Rule,
|
|
434
|
+
created_files: list[str],
|
|
435
|
+
) -> bool:
|
|
436
|
+
"""
|
|
437
|
+
Evaluate a created mode rule.
|
|
438
|
+
|
|
439
|
+
Returns True if rule should fire:
|
|
440
|
+
- At least one created file matches a created pattern
|
|
441
|
+
"""
|
|
442
|
+
for file_path in created_files:
|
|
443
|
+
if matches_any_pattern(file_path, rule.created_patterns):
|
|
444
|
+
return True
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
|
|
421
448
|
@dataclass
|
|
422
449
|
class RuleEvaluationResult:
|
|
423
450
|
"""Result of evaluating a single rule."""
|
|
@@ -428,13 +455,18 @@ class RuleEvaluationResult:
|
|
|
428
455
|
missing_files: list[str] = field(default_factory=list) # For set/pair modes
|
|
429
456
|
|
|
430
457
|
|
|
431
|
-
def evaluate_rule(
|
|
458
|
+
def evaluate_rule(
|
|
459
|
+
rule: Rule,
|
|
460
|
+
changed_files: list[str],
|
|
461
|
+
created_files: list[str] | None = None,
|
|
462
|
+
) -> RuleEvaluationResult:
|
|
432
463
|
"""
|
|
433
464
|
Evaluate whether a rule should fire based on changed files.
|
|
434
465
|
|
|
435
466
|
Args:
|
|
436
467
|
rule: Rule to evaluate
|
|
437
468
|
changed_files: List of changed file paths (relative)
|
|
469
|
+
created_files: List of newly created file paths (relative), for CREATED mode
|
|
438
470
|
|
|
439
471
|
Returns:
|
|
440
472
|
RuleEvaluationResult with evaluation details
|
|
@@ -472,6 +504,20 @@ def evaluate_rule(rule: Rule, changed_files: list[str]) -> RuleEvaluationResult:
|
|
|
472
504
|
missing_files=missing_files,
|
|
473
505
|
)
|
|
474
506
|
|
|
507
|
+
elif rule.detection_mode == DetectionMode.CREATED:
|
|
508
|
+
files_to_check = created_files if created_files is not None else []
|
|
509
|
+
should_fire = evaluate_created(rule, files_to_check)
|
|
510
|
+
trigger_files = (
|
|
511
|
+
[f for f in files_to_check if matches_any_pattern(f, rule.created_patterns)]
|
|
512
|
+
if should_fire
|
|
513
|
+
else []
|
|
514
|
+
)
|
|
515
|
+
return RuleEvaluationResult(
|
|
516
|
+
rule=rule,
|
|
517
|
+
should_fire=should_fire,
|
|
518
|
+
trigger_files=trigger_files,
|
|
519
|
+
)
|
|
520
|
+
|
|
475
521
|
return RuleEvaluationResult(rule=rule, should_fire=False)
|
|
476
522
|
|
|
477
523
|
|
|
@@ -479,6 +525,7 @@ def evaluate_rules(
|
|
|
479
525
|
rules: list[Rule],
|
|
480
526
|
changed_files: list[str],
|
|
481
527
|
promised_rules: set[str] | None = None,
|
|
528
|
+
created_files: list[str] | None = None,
|
|
482
529
|
) -> list[RuleEvaluationResult]:
|
|
483
530
|
"""
|
|
484
531
|
Evaluate which rules should fire.
|
|
@@ -488,6 +535,7 @@ def evaluate_rules(
|
|
|
488
535
|
changed_files: List of changed file paths (relative)
|
|
489
536
|
promised_rules: Set of rule names that have been marked as addressed
|
|
490
537
|
via <promise> tags (case-insensitive)
|
|
538
|
+
created_files: List of newly created file paths (relative), for CREATED mode
|
|
491
539
|
|
|
492
540
|
Returns:
|
|
493
541
|
List of RuleEvaluationResult for rules that should fire
|
|
@@ -504,7 +552,7 @@ def evaluate_rules(
|
|
|
504
552
|
if rule.name.lower() in promised_lower:
|
|
505
553
|
continue
|
|
506
554
|
|
|
507
|
-
result = evaluate_rule(rule, changed_files)
|
|
555
|
+
result = evaluate_rule(rule, changed_files, created_files)
|
|
508
556
|
if result.should_fire:
|
|
509
557
|
results.append(result)
|
|
510
558
|
|
deepwork/hooks/rules_check.py
CHANGED
|
@@ -199,7 +199,176 @@ def get_changed_files_default_tip() -> list[str]:
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def get_changed_files_prompt() -> list[str]:
|
|
202
|
-
"""Get files changed since prompt was submitted.
|
|
202
|
+
"""Get files changed since prompt was submitted.
|
|
203
|
+
|
|
204
|
+
Returns files that changed since the prompt was submitted, including:
|
|
205
|
+
- Committed changes (compared to captured HEAD ref)
|
|
206
|
+
- Staged changes (not yet committed)
|
|
207
|
+
- Untracked files
|
|
208
|
+
|
|
209
|
+
This is used by trigger/safety, set, and pair mode rules to detect
|
|
210
|
+
file modifications during the agent response.
|
|
211
|
+
"""
|
|
212
|
+
baseline_ref_path = Path(".deepwork/.last_head_ref")
|
|
213
|
+
changed_files: set[str] = set()
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Stage all changes first
|
|
217
|
+
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
218
|
+
|
|
219
|
+
# If we have a captured HEAD ref, compare committed changes against it
|
|
220
|
+
if baseline_ref_path.exists():
|
|
221
|
+
baseline_ref = baseline_ref_path.read_text().strip()
|
|
222
|
+
if baseline_ref:
|
|
223
|
+
# Get files changed in commits since the baseline
|
|
224
|
+
result = subprocess.run(
|
|
225
|
+
["git", "diff", "--name-only", baseline_ref, "HEAD"],
|
|
226
|
+
capture_output=True,
|
|
227
|
+
text=True,
|
|
228
|
+
check=False,
|
|
229
|
+
)
|
|
230
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
231
|
+
committed_files = set(result.stdout.strip().split("\n"))
|
|
232
|
+
changed_files.update(f for f in committed_files if f)
|
|
233
|
+
|
|
234
|
+
# Also get currently staged changes (in case not everything is committed)
|
|
235
|
+
result = subprocess.run(
|
|
236
|
+
["git", "diff", "--name-only", "--cached"],
|
|
237
|
+
capture_output=True,
|
|
238
|
+
text=True,
|
|
239
|
+
check=False,
|
|
240
|
+
)
|
|
241
|
+
if result.stdout.strip():
|
|
242
|
+
staged_files = set(result.stdout.strip().split("\n"))
|
|
243
|
+
changed_files.update(f for f in staged_files if f)
|
|
244
|
+
|
|
245
|
+
# Include untracked files
|
|
246
|
+
result = subprocess.run(
|
|
247
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
248
|
+
capture_output=True,
|
|
249
|
+
text=True,
|
|
250
|
+
check=False,
|
|
251
|
+
)
|
|
252
|
+
if result.stdout.strip():
|
|
253
|
+
untracked_files = set(result.stdout.strip().split("\n"))
|
|
254
|
+
changed_files.update(f for f in untracked_files if f)
|
|
255
|
+
|
|
256
|
+
return sorted(changed_files)
|
|
257
|
+
|
|
258
|
+
except (subprocess.CalledProcessError, OSError):
|
|
259
|
+
return []
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_changed_files_for_mode(mode: str) -> list[str]:
|
|
263
|
+
"""Get changed files for a specific compare_to mode."""
|
|
264
|
+
if mode == "base":
|
|
265
|
+
return get_changed_files_base()
|
|
266
|
+
elif mode == "default_tip":
|
|
267
|
+
return get_changed_files_default_tip()
|
|
268
|
+
elif mode == "prompt":
|
|
269
|
+
return get_changed_files_prompt()
|
|
270
|
+
else:
|
|
271
|
+
return get_changed_files_base()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_created_files_base() -> list[str]:
|
|
275
|
+
"""Get files created (added) relative to branch base."""
|
|
276
|
+
default_branch = get_default_branch()
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
result = subprocess.run(
|
|
280
|
+
["git", "merge-base", "HEAD", f"origin/{default_branch}"],
|
|
281
|
+
capture_output=True,
|
|
282
|
+
text=True,
|
|
283
|
+
check=True,
|
|
284
|
+
)
|
|
285
|
+
merge_base = result.stdout.strip()
|
|
286
|
+
|
|
287
|
+
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
288
|
+
|
|
289
|
+
# Get only added files (not modified) using --diff-filter=A
|
|
290
|
+
result = subprocess.run(
|
|
291
|
+
["git", "diff", "--name-only", "--diff-filter=A", merge_base, "HEAD"],
|
|
292
|
+
capture_output=True,
|
|
293
|
+
text=True,
|
|
294
|
+
check=True,
|
|
295
|
+
)
|
|
296
|
+
committed_added = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
297
|
+
|
|
298
|
+
# Staged new files that don't exist in merge_base
|
|
299
|
+
result = subprocess.run(
|
|
300
|
+
["git", "diff", "--name-only", "--diff-filter=A", "--cached", merge_base],
|
|
301
|
+
capture_output=True,
|
|
302
|
+
text=True,
|
|
303
|
+
check=False,
|
|
304
|
+
)
|
|
305
|
+
staged_added = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
306
|
+
|
|
307
|
+
# Untracked files are by definition "created"
|
|
308
|
+
result = subprocess.run(
|
|
309
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
310
|
+
capture_output=True,
|
|
311
|
+
text=True,
|
|
312
|
+
check=False,
|
|
313
|
+
)
|
|
314
|
+
untracked_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
315
|
+
|
|
316
|
+
all_created = committed_added | staged_added | untracked_files
|
|
317
|
+
return sorted([f for f in all_created if f])
|
|
318
|
+
|
|
319
|
+
except subprocess.CalledProcessError:
|
|
320
|
+
return []
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_created_files_default_tip() -> list[str]:
|
|
324
|
+
"""Get files created compared to default branch tip."""
|
|
325
|
+
default_branch = get_default_branch()
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
subprocess.run(["git", "add", "-A"], capture_output=True, check=False)
|
|
329
|
+
|
|
330
|
+
# Get only added files using --diff-filter=A
|
|
331
|
+
result = subprocess.run(
|
|
332
|
+
["git", "diff", "--name-only", "--diff-filter=A", f"origin/{default_branch}..HEAD"],
|
|
333
|
+
capture_output=True,
|
|
334
|
+
text=True,
|
|
335
|
+
check=True,
|
|
336
|
+
)
|
|
337
|
+
committed_added = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
338
|
+
|
|
339
|
+
result = subprocess.run(
|
|
340
|
+
[
|
|
341
|
+
"git",
|
|
342
|
+
"diff",
|
|
343
|
+
"--name-only",
|
|
344
|
+
"--diff-filter=A",
|
|
345
|
+
"--cached",
|
|
346
|
+
f"origin/{default_branch}",
|
|
347
|
+
],
|
|
348
|
+
capture_output=True,
|
|
349
|
+
text=True,
|
|
350
|
+
check=False,
|
|
351
|
+
)
|
|
352
|
+
staged_added = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
353
|
+
|
|
354
|
+
# Untracked files are by definition "created"
|
|
355
|
+
result = subprocess.run(
|
|
356
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
357
|
+
capture_output=True,
|
|
358
|
+
text=True,
|
|
359
|
+
check=False,
|
|
360
|
+
)
|
|
361
|
+
untracked_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
362
|
+
|
|
363
|
+
all_created = committed_added | staged_added | untracked_files
|
|
364
|
+
return sorted([f for f in all_created if f])
|
|
365
|
+
|
|
366
|
+
except subprocess.CalledProcessError:
|
|
367
|
+
return []
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def get_created_files_prompt() -> list[str]:
|
|
371
|
+
"""Get files created since prompt was submitted."""
|
|
203
372
|
baseline_path = Path(".deepwork/.last_work_tree")
|
|
204
373
|
|
|
205
374
|
try:
|
|
@@ -214,28 +383,42 @@ def get_changed_files_prompt() -> list[str]:
|
|
|
214
383
|
current_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
215
384
|
current_files = {f for f in current_files if f}
|
|
216
385
|
|
|
386
|
+
# Untracked files
|
|
387
|
+
result = subprocess.run(
|
|
388
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
389
|
+
capture_output=True,
|
|
390
|
+
text=True,
|
|
391
|
+
check=False,
|
|
392
|
+
)
|
|
393
|
+
untracked_files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
|
|
394
|
+
untracked_files = {f for f in untracked_files if f}
|
|
395
|
+
|
|
396
|
+
all_current = current_files | untracked_files
|
|
397
|
+
|
|
217
398
|
if baseline_path.exists():
|
|
218
399
|
baseline_files = set(baseline_path.read_text().strip().split("\n"))
|
|
219
400
|
baseline_files = {f for f in baseline_files if f}
|
|
220
|
-
|
|
221
|
-
|
|
401
|
+
# Created files are those that didn't exist at baseline
|
|
402
|
+
created_files = all_current - baseline_files
|
|
403
|
+
return sorted(created_files)
|
|
222
404
|
else:
|
|
223
|
-
|
|
405
|
+
# No baseline means all current files are "new" to this prompt
|
|
406
|
+
return sorted(all_current)
|
|
224
407
|
|
|
225
408
|
except (subprocess.CalledProcessError, OSError):
|
|
226
409
|
return []
|
|
227
410
|
|
|
228
411
|
|
|
229
|
-
def
|
|
230
|
-
"""Get
|
|
412
|
+
def get_created_files_for_mode(mode: str) -> list[str]:
|
|
413
|
+
"""Get created files for a specific compare_to mode."""
|
|
231
414
|
if mode == "base":
|
|
232
|
-
return
|
|
415
|
+
return get_created_files_base()
|
|
233
416
|
elif mode == "default_tip":
|
|
234
|
-
return
|
|
417
|
+
return get_created_files_default_tip()
|
|
235
418
|
elif mode == "prompt":
|
|
236
|
-
return
|
|
419
|
+
return get_created_files_prompt()
|
|
237
420
|
else:
|
|
238
|
-
return
|
|
421
|
+
return get_created_files_base()
|
|
239
422
|
|
|
240
423
|
|
|
241
424
|
def extract_promise_tags(text: str) -> set[str]:
|
|
@@ -399,13 +582,16 @@ def rules_check_hook(hook_input: HookInput) -> HookOutput:
|
|
|
399
582
|
|
|
400
583
|
for mode, mode_rules in rules_by_mode.items():
|
|
401
584
|
changed_files = get_changed_files_for_mode(mode)
|
|
402
|
-
|
|
585
|
+
created_files = get_created_files_for_mode(mode)
|
|
586
|
+
|
|
587
|
+
# Skip if no changed or created files
|
|
588
|
+
if not changed_files and not created_files:
|
|
403
589
|
continue
|
|
404
590
|
|
|
405
591
|
baseline_ref = get_baseline_ref(mode)
|
|
406
592
|
|
|
407
593
|
# Evaluate which rules fire
|
|
408
|
-
results = evaluate_rules(mode_rules, changed_files, promised_rules)
|
|
594
|
+
results = evaluate_rules(mode_rules, changed_files, promised_rules, created_files)
|
|
409
595
|
|
|
410
596
|
for result in results:
|
|
411
597
|
rule = result.rule
|
deepwork/schemas/rules_schema.py
CHANGED
|
@@ -15,7 +15,7 @@ STRING_OR_ARRAY: dict[str, Any] = {
|
|
|
15
15
|
RULES_FRONTMATTER_SCHEMA: dict[str, Any] = {
|
|
16
16
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
17
17
|
"type": "object",
|
|
18
|
-
"required": ["name"],
|
|
18
|
+
"required": ["name", "compare_to"],
|
|
19
19
|
"properties": {
|
|
20
20
|
"name": {
|
|
21
21
|
"type": "string",
|
|
@@ -56,6 +56,11 @@ RULES_FRONTMATTER_SCHEMA: dict[str, Any] = {
|
|
|
56
56
|
"additionalProperties": False,
|
|
57
57
|
"description": "Directional file correspondence (trigger -> expects)",
|
|
58
58
|
},
|
|
59
|
+
# Detection mode: created (fire when files are created matching patterns)
|
|
60
|
+
"created": {
|
|
61
|
+
**STRING_OR_ARRAY,
|
|
62
|
+
"description": "Glob pattern(s) for newly created files that trigger this rule",
|
|
63
|
+
},
|
|
59
64
|
# Action type: command (default is prompt using markdown body)
|
|
60
65
|
"action": {
|
|
61
66
|
"type": "object",
|
|
@@ -80,24 +85,51 @@ RULES_FRONTMATTER_SCHEMA: dict[str, Any] = {
|
|
|
80
85
|
"compare_to": {
|
|
81
86
|
"type": "string",
|
|
82
87
|
"enum": ["base", "default_tip", "prompt"],
|
|
83
|
-
"default": "base",
|
|
84
88
|
"description": "Baseline for detecting file changes",
|
|
85
89
|
},
|
|
86
90
|
},
|
|
87
91
|
"additionalProperties": False,
|
|
88
|
-
# Detection mode must be exactly one of: trigger, set, or
|
|
92
|
+
# Detection mode must be exactly one of: trigger, set, pair, or created
|
|
89
93
|
"oneOf": [
|
|
90
94
|
{
|
|
91
95
|
"required": ["trigger"],
|
|
92
|
-
"not": {
|
|
96
|
+
"not": {
|
|
97
|
+
"anyOf": [
|
|
98
|
+
{"required": ["set"]},
|
|
99
|
+
{"required": ["pair"]},
|
|
100
|
+
{"required": ["created"]},
|
|
101
|
+
]
|
|
102
|
+
},
|
|
93
103
|
},
|
|
94
104
|
{
|
|
95
105
|
"required": ["set"],
|
|
96
|
-
"not": {
|
|
106
|
+
"not": {
|
|
107
|
+
"anyOf": [
|
|
108
|
+
{"required": ["trigger"]},
|
|
109
|
+
{"required": ["pair"]},
|
|
110
|
+
{"required": ["created"]},
|
|
111
|
+
]
|
|
112
|
+
},
|
|
97
113
|
},
|
|
98
114
|
{
|
|
99
115
|
"required": ["pair"],
|
|
100
|
-
"not": {
|
|
116
|
+
"not": {
|
|
117
|
+
"anyOf": [
|
|
118
|
+
{"required": ["trigger"]},
|
|
119
|
+
{"required": ["set"]},
|
|
120
|
+
{"required": ["created"]},
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"required": ["created"],
|
|
126
|
+
"not": {
|
|
127
|
+
"anyOf": [
|
|
128
|
+
{"required": ["trigger"]},
|
|
129
|
+
{"required": ["set"]},
|
|
130
|
+
{"required": ["pair"]},
|
|
131
|
+
]
|
|
132
|
+
},
|
|
101
133
|
},
|
|
102
134
|
],
|
|
103
135
|
}
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# capture_prompt_work_tree.sh - Captures the git work tree state at prompt submission
|
|
3
3
|
#
|
|
4
|
-
# This script creates a snapshot of
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# This script creates a snapshot of ALL tracked files at the time the prompt
|
|
5
|
+
# is submitted. This baseline is used for rules with compare_to: prompt and
|
|
6
|
+
# created: mode to detect truly NEW files (not modifications to existing ones).
|
|
7
|
+
#
|
|
8
|
+
# The baseline contains ALL tracked files (not just changed files) so that
|
|
9
|
+
# the rules_check hook can determine which files are genuinely new vs which
|
|
10
|
+
# files existed before and were just modified.
|
|
8
11
|
|
|
9
12
|
set -e
|
|
10
13
|
|
|
11
14
|
# Ensure .deepwork directory exists
|
|
12
15
|
mkdir -p .deepwork
|
|
13
16
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
git diff --name-only HEAD > .deepwork/.last_work_tree 2>/dev/null || true
|
|
17
|
+
# Save ALL tracked files (not just changed files)
|
|
18
|
+
# This is critical for created: mode rules to distinguish between:
|
|
19
|
+
# - Newly created files (not in baseline) -> should trigger created: rules
|
|
20
|
+
# - Modified existing files (in baseline) -> should NOT trigger created: rules
|
|
21
|
+
git ls-files > .deepwork/.last_work_tree 2>/dev/null || true
|
|
20
22
|
|
|
21
|
-
# Also include untracked files
|
|
23
|
+
# Also include untracked files that exist at prompt time
|
|
24
|
+
# These are files the user may have created before submitting the prompt
|
|
22
25
|
git ls-files --others --exclude-standard >> .deepwork/.last_work_tree 2>/dev/null || true
|
|
23
26
|
|
|
24
27
|
# Sort and deduplicate
|
|
@@ -6,10 +6,17 @@ description: |
|
|
|
6
6
|
Rules help ensure that code changes follow team guidelines, documentation is updated,
|
|
7
7
|
and architectural decisions are respected.
|
|
8
8
|
|
|
9
|
+
IMPORTANT: Rules are evaluated at the "Stop" hook, which fires when an agent finishes its turn.
|
|
10
|
+
This includes when sub-agents complete their work. Rules are NOT evaluated immediately after
|
|
11
|
+
each file edit - they batch up and run once at the end of the agent's response cycle.
|
|
12
|
+
- Command action rules: Execute their command (e.g., `uv sync`) when the agent stops
|
|
13
|
+
- Prompt action rules: Display instructions to the agent, blocking until addressed
|
|
14
|
+
|
|
9
15
|
Rules are stored as individual markdown files with YAML frontmatter in the `.deepwork/rules/`
|
|
10
16
|
directory. Each rule file specifies:
|
|
11
17
|
- Detection mode: trigger/safety, set (bidirectional), or pair (directional)
|
|
12
18
|
- Patterns: Glob patterns for matching files, with optional variable capture
|
|
19
|
+
- Action type: prompt (default) to show instructions, or command to run a shell command
|
|
13
20
|
- Instructions: Markdown content describing what the agent should do
|
|
14
21
|
|
|
15
22
|
Example use cases:
|
|
@@ -17,6 +24,7 @@ description: |
|
|
|
17
24
|
- Require security review when authentication code is modified
|
|
18
25
|
- Ensure API documentation stays in sync with API code
|
|
19
26
|
- Enforce source/test file pairing
|
|
27
|
+
- Auto-run `uv sync` when pyproject.toml changes (command action)
|
|
20
28
|
|
|
21
29
|
changelog:
|
|
22
30
|
- version: "0.1.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepwork
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Framework for enabling AI agents to perform complex, multi-step work tasks
|
|
5
5
|
Project-URL: Homepage, https://github.com/deepwork/deepwork
|
|
6
6
|
Project-URL: Documentation, https://github.com/deepwork/deepwork#readme
|
|
@@ -297,6 +297,7 @@ name: Source/Test Pairing
|
|
|
297
297
|
set:
|
|
298
298
|
- src/{path}.py
|
|
299
299
|
- tests/{path}_test.py
|
|
300
|
+
compare_to: base
|
|
300
301
|
---
|
|
301
302
|
When source files change, corresponding test files should also change.
|
|
302
303
|
Please create or update tests for the modified source files.
|
|
@@ -310,6 +311,7 @@ trigger: "**/*.py"
|
|
|
310
311
|
action:
|
|
311
312
|
command: "ruff format {file}"
|
|
312
313
|
run_for: each_match
|
|
314
|
+
compare_to: prompt
|
|
313
315
|
---
|
|
314
316
|
```
|
|
315
317
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
deepwork/__init__.py,sha256=vcMnJioxhfoL6kGh4FM51Vk9UoLnQ76g6Ms7XDUItYA,748
|
|
2
2
|
deepwork/cli/__init__.py,sha256=3SqmfcP2xqutiCYAbajFDJTjr2pOLydqTN0NN-FTsIE,33
|
|
3
|
-
deepwork/cli/install.py,sha256=
|
|
3
|
+
deepwork/cli/install.py,sha256=USSmALGWpCBc4kinAxTbt8j2AcSJ4vVNtYpG5s0M0sE,12897
|
|
4
4
|
deepwork/cli/main.py,sha256=m6tVnfSy03azI9Ky6ySEMat_UdLEMzVT3SsRKQWigvA,471
|
|
5
5
|
deepwork/cli/sync.py,sha256=d608WDOzOVpYbS6RF-xoonK3qj22WyewRFJrva_qXaQ,6251
|
|
6
6
|
deepwork/core/__init__.py,sha256=1g869QuwsYzNjQONneng2OMc6HKt-tlBCaxJbMMfoho,36
|
|
@@ -11,17 +11,17 @@ deepwork/core/generator.py,sha256=0tauPRUA_D9o6BEksx9BJFV9lOlpgK1Te8fI2gxP0kc,14
|
|
|
11
11
|
deepwork/core/hooks_syncer.py,sha256=kEr8bLgWJXPb1C8AOv2fxUnIX1j9zgfPahyYIZUvfko,6771
|
|
12
12
|
deepwork/core/parser.py,sha256=R799f_IC9zhQNROnIC534LRZ_TMYp5gaqwH3nGSrI9Y,9936
|
|
13
13
|
deepwork/core/pattern_matcher.py,sha256=wtrxFNSCMxk-47Nw4Ca-8769ll4weRvvQBLGsRERcKM,8073
|
|
14
|
-
deepwork/core/rules_parser.py,sha256=
|
|
14
|
+
deepwork/core/rules_parser.py,sha256=r_Pay0Ug9FDGCEccpLnQjsQjSEUmECceh0Ac3Ayw4oE,17774
|
|
15
15
|
deepwork/core/rules_queue.py,sha256=uCNuQzohUyEYGnCUv1nMDEYoLeTuiSHN44-kuXbSLDU,10069
|
|
16
16
|
deepwork/hooks/README.md,sha256=t4rAd68oRx0-cbln4kfV0Sk2IWhGAGR7HB4McgBXTYE,4391
|
|
17
17
|
deepwork/hooks/__init__.py,sha256=aUWXT5b8MaKzYp7x2Bwezj_NQwAJ-6a8nXxDz0o0qsY,1882
|
|
18
18
|
deepwork/hooks/claude_hook.sh,sha256=VjbRVOUirIhFU7CcsafWk25HTIUzZmCrWqNPiY9e17o,1539
|
|
19
19
|
deepwork/hooks/gemini_hook.sh,sha256=wVfnZs7VvUNFgOkZnhUpbMYlVyy7lQWG_u8PqDQbL8g,1534
|
|
20
|
-
deepwork/hooks/rules_check.py,sha256=
|
|
20
|
+
deepwork/hooks/rules_check.py,sha256=dmXnxOkpT1KH32uQkYeb_Btwykykb4-Gr4H4C86IIfo,23562
|
|
21
21
|
deepwork/hooks/wrapper.py,sha256=mSps8tWQEdBThXwDViOtNad0d9pFaNPF8MFpG-Rhuds,11517
|
|
22
22
|
deepwork/schemas/__init__.py,sha256=PpydKb_oaTv8lYapN_nV-Tl_OUCoSM_okvsEJ8gNTpI,41
|
|
23
23
|
deepwork/schemas/job_schema.py,sha256=R_82YmLWGn6lV1arjJ7_EzYFMYoTy1tzdUBk34mdNSY,9388
|
|
24
|
-
deepwork/schemas/rules_schema.py,sha256=
|
|
24
|
+
deepwork/schemas/rules_schema.py,sha256=CNv0_ambXTHZKFy81VCpew6KmHslSQPwLG7yd2x-z_0,4666
|
|
25
25
|
deepwork/standard_jobs/deepwork_jobs/AGENTS.md,sha256=Y6I4jZ8DfN0RFY3UF5bgQRZvL7wQD9P0lgE7EZM6CGI,2252
|
|
26
26
|
deepwork/standard_jobs/deepwork_jobs/job.yml,sha256=fNZVh_iW0sUXUCs_2YcIi2CVVLcnrM95YE9Zq4cZeNI,5399
|
|
27
27
|
deepwork/standard_jobs/deepwork_jobs/make_new_job.sh,sha256=JArfFU9lEaJPRsXRL3rU1IMt2p6Bq0s2C9f98aJ7Mxg,3878
|
|
@@ -34,15 +34,15 @@ deepwork/standard_jobs/deepwork_jobs/templates/job.yml.example,sha256=roRi6sIGFG
|
|
|
34
34
|
deepwork/standard_jobs/deepwork_jobs/templates/job.yml.template,sha256=3I-VQvqXVJNZ_Vb5Ik28JsrEbGKbyLTZcuKxdEmV5s0,1789
|
|
35
35
|
deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.example,sha256=HXcjVaQz2HsDiA4ClnIeLvysVOGrFJ_5Tr-pm6dhdwc,2706
|
|
36
36
|
deepwork/standard_jobs/deepwork_jobs/templates/step_instruction.md.template,sha256=6n9jFFuda4r549Oo-LBPKixFD3NvDl5MwEg5V7ItQBg,1286
|
|
37
|
-
deepwork/standard_jobs/deepwork_rules/job.yml,sha256=
|
|
38
|
-
deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh,sha256=
|
|
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
39
|
deepwork/standard_jobs/deepwork_rules/hooks/global_hooks.yml,sha256=uZkGMROaICKik6-Mfa1dM-8608Y9L-VrYG5YAbVN8yw,186
|
|
40
40
|
deepwork/standard_jobs/deepwork_rules/hooks/user_prompt_submit.sh,sha256=TxwYb7kBW-cfHmcQoruJBjCTWvdWbQVQIMplNgzMuOs,498
|
|
41
41
|
deepwork/standard_jobs/deepwork_rules/rules/.gitkeep,sha256=tOhdLTAbVXm_Z4Gl85recPNonTC0kFwzw6NKpeqTmH8,337
|
|
42
42
|
deepwork/standard_jobs/deepwork_rules/rules/api-documentation-sync.md.example,sha256=YaTuv-LseeqYSG6mk2LzORfyGJ0bGaXuTB8IJkwzrDI,260
|
|
43
43
|
deepwork/standard_jobs/deepwork_rules/rules/readme-documentation.md.example,sha256=2uZHS-LjZfL81i7COJvIA0Q5lqa-aWyLx060ouSnlrw,305
|
|
44
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=
|
|
45
|
+
deepwork/standard_jobs/deepwork_rules/rules/skill-md-validation.md,sha256=H-o8X92AerumY2nimLzX-PniN2FGWAPI4tNGMpEo7nM,1457
|
|
46
46
|
deepwork/standard_jobs/deepwork_rules/rules/source-test-pairing.md.example,sha256=Uwg0l85nB69yY8MWwF46EUwx3UJ3qIHXb2ywxp_VNW0,320
|
|
47
47
|
deepwork/standard_jobs/deepwork_rules/steps/define.md,sha256=TP3j90bh0zsGQniAo2GkX9xbLtB_XkaA4IudcbAZ0YY,8349
|
|
48
48
|
deepwork/templates/__init__.py,sha256=APvjx_u7eRUerw9yA_fJ1ZqCzYA-FWUCV9HCz0RgjOc,50
|
|
@@ -55,8 +55,8 @@ deepwork/utils/fs.py,sha256=94OUvUrqGebjHVtnjd5vXL6DalKNdpRu-iAPsHvAPjI,3499
|
|
|
55
55
|
deepwork/utils/git.py,sha256=J4tAB1zE6-WMAEHbarevhmSvvPLkeKBpiRv1UxUVwYk,3748
|
|
56
56
|
deepwork/utils/validation.py,sha256=SyFg9fIu1JCDMbssQgJRCTUNToDNcINccn8lje-tjts,851
|
|
57
57
|
deepwork/utils/yaml_utils.py,sha256=X8c9yEqxEgw5CdPQ23f1Wz8SSP783MMGKyGV_2SKaNU,2454
|
|
58
|
-
deepwork-0.3.
|
|
59
|
-
deepwork-0.3.
|
|
60
|
-
deepwork-0.3.
|
|
61
|
-
deepwork-0.3.
|
|
62
|
-
deepwork-0.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|