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 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
- supported = ", ".join(
287
- f"{AgentAdapter.get(name).display_name} ({AgentAdapter.get(name).config_dir}/)"
288
- for name in AgentAdapter.list_names()
289
- )
290
- raise InstallError(
291
- f"No AI platform detected.\n"
292
- f"DeepWork supports: {supported}.\n"
293
- "Please set up one of these platforms first, or use --platform to specify."
294
- )
295
-
296
- # Add all detected platforms
297
- for adapter in available_adapters:
298
- console.print(f" [green]✓[/green] {adapter.display_name} detected")
299
- platforms_to_add.append(adapter.name)
300
- detected_adapters = available_adapters
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...")
@@ -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 'pair'")
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.get("compare_to", DEFAULT_COMPARE_TO)
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(rule: Rule, changed_files: list[str]) -> RuleEvaluationResult:
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
 
@@ -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
- new_files = current_files - baseline_files
221
- return sorted(new_files)
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
- return sorted(current_files)
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 get_changed_files_for_mode(mode: str) -> list[str]:
230
- """Get changed files for a specific compare_to mode."""
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 get_changed_files_base()
415
+ return get_created_files_base()
233
416
  elif mode == "default_tip":
234
- return get_changed_files_default_tip()
417
+ return get_created_files_default_tip()
235
418
  elif mode == "prompt":
236
- return get_changed_files_prompt()
419
+ return get_created_files_prompt()
237
420
  else:
238
- return get_changed_files_base()
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
- if not changed_files:
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
@@ -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 pair
92
+ # Detection mode must be exactly one of: trigger, set, pair, or created
89
93
  "oneOf": [
90
94
  {
91
95
  "required": ["trigger"],
92
- "not": {"anyOf": [{"required": ["set"]}, {"required": ["pair"]}]},
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": {"anyOf": [{"required": ["trigger"]}, {"required": ["pair"]}]},
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": {"anyOf": [{"required": ["trigger"]}, {"required": ["set"]}]},
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 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).
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
- # 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
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 not yet in the index
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,7 @@
1
1
  ---
2
2
  name: SKILL.md Validation
3
3
  trigger: "**/SKILL.md"
4
+ compare_to: base
4
5
  ---
5
6
  A SKILL.md file has been created or modified. Please validate that it follows the required format:
6
7
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepwork
3
- Version: 0.3.0
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=KmeTAndLP-5LXfhVwvbtq64jtAJKXYcj-ce4bt1ylO8,12643
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=VR51HwdX5qUHycHsEK51z7Btn_mCc5wpQYBMKf7bnpc,16027
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=LCblFU4rNxzhff4mJi0wYRngGbS6636U056jNJfX3bA,16813
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=lysYmixrl8iaND4JIfnlkPB9ejkYogMZR3qsZj1Q9Ag,3755
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=IGmH92YjOFQnlWMLyys9hSBADqR_27w7g3Mnl0DPYcE,1552
38
- deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh,sha256=D6Ozo9oDqsL7YBh-ebQK1S8ED9hfIi_0Z8khFjC6wZY,973
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=ZHAD5nMOdU2m5ffsuUidQtaACfvudSZx9LBJo5OWKFY,1440
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.0.dist-info/METADATA,sha256=GbJatDr9S6nIdveZ8D26gdiGV1QNS3mOAHA2el_LfGw,12241
59
- deepwork-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
60
- deepwork-0.3.0.dist-info/entry_points.txt,sha256=RhJBySzm619kh-yIdsAyfFXInAlY8Jm-39FLIBcOj2s,51
61
- deepwork-0.3.0.dist-info/licenses/LICENSE.md,sha256=W0EtJVYf0cQ_awukOCW1ETwNSpV2RKqnAGfoOjyz_K8,4126
62
- deepwork-0.3.0.dist-info/RECORD,,
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,,