Skip to content

Commit 7680c3b

Browse files
committed
Add 'created' detection mode for rules system
Implements a new rule detection mode that only triggers when files are newly created, not when existing files are modified. This enables rules to enforce standards specifically for new code. - Add CREATED detection mode to DetectionMode enum - Add created_patterns field to Rule dataclass - Update schema to validate 'created' field - Implement evaluate_created() function - Add get_created_files_* functions for each compare_to mode - Update evaluate_rule() and evaluate_rules() to handle created mode - Update documentation with examples and field reference - Add comprehensive tests for created mode (10 new tests)
1 parent 53bd81e commit 7680c3b

6 files changed

Lines changed: 581 additions & 11 deletions

File tree

doc/rules_syntax.md

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ This rule runs `ruff format` on any changed Python files to ensure
9595
consistent code style across the codebase.
9696
```
9797

98+
### Created Mode (file creation trigger)
99+
100+
`.deepwork/rules/new-module-docs.md`:
101+
```markdown
102+
---
103+
name: New Module Documentation
104+
created: src/**/*.py
105+
---
106+
A new Python module was created. Please ensure:
107+
108+
- Add module docstring explaining the purpose
109+
- Update relevant documentation if adding a public API
110+
- Consider adding tests for the new module
111+
```
112+
98113
## Rule Structure
99114

100115
Every rule has two orthogonal aspects:
@@ -108,6 +123,7 @@ How the rule decides when to fire:
108123
| **Trigger/Safety** | `trigger`, `safety` | Fire when trigger matches and safety doesn't |
109124
| **Set** | `set` | Fire when file correspondence is incomplete (bidirectional) |
110125
| **Pair** | `pair` | Fire when file correspondence is incomplete (directional) |
126+
| **Created** | `created` | Fire when newly created files match patterns |
111127

112128
### Action Type
113129

@@ -198,6 +214,47 @@ If `api/users/create.py` changes:
198214
If `docs/api/users/create.md` changes alone:
199215
- No trigger (documentation can be updated independently)
200216

217+
### Created Mode (File Creation Detection)
218+
219+
Fires only when files are newly created (not modified). Useful for enforcing standards on new files.
220+
221+
```yaml
222+
---
223+
name: New Component Documentation
224+
created:
225+
- src/components/**/*.tsx
226+
- src/components/**/*.ts
227+
---
228+
```
229+
230+
**How it works:**
231+
232+
1. A file is created that matches a `created` pattern
233+
2. Rule fires with instructions
234+
235+
Key differences from Trigger/Safety mode:
236+
- Only fires for **new** files, not modifications to existing files
237+
- No safety patterns (use Trigger/Safety mode if you need safety)
238+
- Good for enforcing documentation, tests, or standards on new code
239+
240+
**Examples:**
241+
242+
```yaml
243+
# Single pattern
244+
created: src/api/**/*.py
245+
246+
# Multiple patterns
247+
created:
248+
- src/models/**/*.py
249+
- src/services/**/*.py
250+
```
251+
252+
If a new file `src/api/users.py` is created:
253+
- Rule fires with instructions for new API modules
254+
255+
If an existing file `src/api/users.py` is modified:
256+
- Rule does NOT fire (file already existed)
257+
201258
## Action Types
202259

203260
### Prompt Action (Default)
@@ -373,6 +430,22 @@ pair:
373430
---
374431
```
375432

433+
### created
434+
435+
File patterns that trigger when files are newly created (created mode). Only fires for new files, not modifications. Can be string or array.
436+
437+
```yaml
438+
---
439+
created: src/**/*.py
440+
---
441+
442+
---
443+
created:
444+
- src/**/*.py
445+
- lib/**/*.py
446+
---
447+
```
448+
376449
### action (optional)
377450

378451
Specifies a command to run instead of prompting.
@@ -503,6 +576,39 @@ This rule is suppressed if you've already modified pyproject.toml
503576
or CHANGELOG.md, as that indicates you're handling versioning.
504577
```
505578

579+
### Example 6: New File Standards (Created Mode)
580+
581+
`.deepwork/rules/new-module-standards.md`:
582+
```markdown
583+
---
584+
name: New Module Standards
585+
created:
586+
- src/**/*.py
587+
- lib/**/*.py
588+
---
589+
A new Python module was created. Please ensure it follows our standards:
590+
591+
1. **Module docstring**: Add a docstring at the top explaining the module's purpose
592+
2. **Type hints**: Use type hints for all function parameters and return values
593+
3. **Tests**: Create a corresponding test file in tests/
594+
4. **Imports**: Follow the import order (stdlib, third-party, local)
595+
596+
This rule only fires for newly created files, not modifications.
597+
```
598+
599+
### Example 7: New Component Checklist (Created Mode with Command)
600+
601+
`.deepwork/rules/new-component-lint.md`:
602+
```markdown
603+
---
604+
name: New Component Lint
605+
created: src/components/**/*.tsx
606+
action:
607+
command: eslint --fix {file}
608+
---
609+
Automatically lints newly created React components.
610+
```
611+
506612
## Promise Tags
507613

508614
When a rule fires but should be dismissed, use promise tags in the conversation. The tag content should be human-readable, using the rule's `name` field:
@@ -525,7 +631,7 @@ Error: .deepwork/rules/my-rule.md - invalid YAML frontmatter
525631
526632
**Missing required field:**
527633
```
528-
Error: .deepwork/rules/my-rule.md - must have 'trigger', 'set', or 'pair'
634+
Error: .deepwork/rules/my-rule.md - must have 'trigger', 'set', 'pair', or 'created'
529635
```
530636
531637
**Invalid pattern:**

doc/rules_system_design.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Every rule has two orthogonal aspects:
2222
| **Trigger/Safety** | `trigger`, `safety` | Fire when trigger matches and safety doesn't |
2323
| **Set** | `set` | Fire when file correspondence is incomplete (bidirectional) |
2424
| **Pair** | `pair` | Fire when file correspondence is incomplete (directional) |
25+
| **Created** | `created` | Fire when newly created files match patterns |
2526

2627
**Action Type** - What happens when the rule fires:
2728

@@ -47,6 +48,12 @@ Every rule has two orthogonal aspects:
4748
- Changes to expected files alone do not trigger the rule
4849
- Example: API code requires documentation updates
4950

51+
**Created Mode (File Creation Detection)**
52+
- Define patterns for newly created files
53+
- Only fires when files are created, not when existing files are modified
54+
- Useful for enforcing standards on new code (documentation, tests, etc.)
55+
- Example: New modules require documentation and tests
56+
5057
### Pattern Variables
5158

5259
Patterns use `{name}` syntax for capturing variable path segments:
@@ -288,6 +295,21 @@ the pair rule does NOT trigger (directional).
288295
7. Evaluator: If changes keep occurring, mark .failed, alert user
289296
```
290297

298+
### Created Rule
299+
300+
```
301+
1. Detector: New file created, matches "src/**/*.py" created pattern
302+
2. Detector: Verify file is newly created (not just modified)
303+
3. Detector: Create .queued entry for new file rule
304+
4. Evaluator: Return instructions for new file standards
305+
5. Agent: Addresses rule, includes <promise> tag
306+
6. Evaluator: On next check, mark .passed (promise found)
307+
```
308+
309+
Note: Created mode uses separate file detection to distinguish newly
310+
created files from modified files. Untracked files and files added
311+
since the baseline are considered "created".
312+
291313
## Agent Output Management
292314

293315
### Problem

src/deepwork/core/rules_parser.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class DetectionMode(Enum):
2929
TRIGGER_SAFETY = "trigger_safety" # Fire when trigger matches, safety doesn't
3030
SET = "set" # Bidirectional file correspondence
3131
PAIR = "pair" # Directional file correspondence
32+
CREATED = "created" # Fire when created files match patterns
3233

3334

3435
class ActionType(Enum):
@@ -73,6 +74,7 @@ class Rule:
7374
safety: list[str] = field(default_factory=list) # For TRIGGER_SAFETY mode
7475
set_patterns: list[str] = field(default_factory=list) # For SET mode
7576
pair_config: PairConfig | None = None # For PAIR mode
77+
created_patterns: list[str] = field(default_factory=list) # For CREATED mode
7678

7779
# Action type
7880
action_type: ActionType = ActionType.PROMPT
@@ -112,10 +114,13 @@ def from_frontmatter(
112114
has_trigger = "trigger" in frontmatter
113115
has_set = "set" in frontmatter
114116
has_pair = "pair" in frontmatter
117+
has_created = "created" in frontmatter
115118

116-
mode_count = sum([has_trigger, has_set, has_pair])
119+
mode_count = sum([has_trigger, has_set, has_pair, has_created])
117120
if mode_count == 0:
118-
raise RulesParseError(f"Rule '{name}' must have 'trigger', 'set', or 'pair'")
121+
raise RulesParseError(
122+
f"Rule '{name}' must have 'trigger', 'set', 'pair', or 'created'"
123+
)
119124
if mode_count > 1:
120125
raise RulesParseError(f"Rule '{name}' has multiple detection modes - use only one")
121126

@@ -125,6 +130,7 @@ def from_frontmatter(
125130
safety: list[str] = []
126131
set_patterns: list[str] = []
127132
pair_config: PairConfig | None = None
133+
created_patterns: list[str] = []
128134

129135
if has_trigger:
130136
detection_mode = DetectionMode.TRIGGER_SAFETY
@@ -149,6 +155,11 @@ def from_frontmatter(
149155
expects=expects_list,
150156
)
151157

158+
elif has_created:
159+
detection_mode = DetectionMode.CREATED
160+
created = frontmatter["created"]
161+
created_patterns = [created] if isinstance(created, str) else list(created)
162+
152163
# Determine action type
153164
action_type: ActionType
154165
command_action: CommandAction | None = None
@@ -177,6 +188,7 @@ def from_frontmatter(
177188
safety=safety,
178189
set_patterns=set_patterns,
179190
pair_config=pair_config,
191+
created_patterns=created_patterns,
180192
action_type=action_type,
181193
instructions=markdown_body.strip(),
182194
command_action=command_action,
@@ -418,6 +430,22 @@ def evaluate_pair_correspondence(
418430
return should_fire, trigger_files, missing_files
419431

420432

433+
def evaluate_created(
434+
rule: Rule,
435+
created_files: list[str],
436+
) -> bool:
437+
"""
438+
Evaluate a created mode rule.
439+
440+
Returns True if rule should fire:
441+
- At least one created file matches a created pattern
442+
"""
443+
for file_path in created_files:
444+
if matches_any_pattern(file_path, rule.created_patterns):
445+
return True
446+
return False
447+
448+
421449
@dataclass
422450
class RuleEvaluationResult:
423451
"""Result of evaluating a single rule."""
@@ -428,13 +456,18 @@ class RuleEvaluationResult:
428456
missing_files: list[str] = field(default_factory=list) # For set/pair modes
429457

430458

431-
def evaluate_rule(rule: Rule, changed_files: list[str]) -> RuleEvaluationResult:
459+
def evaluate_rule(
460+
rule: Rule,
461+
changed_files: list[str],
462+
created_files: list[str] | None = None,
463+
) -> RuleEvaluationResult:
432464
"""
433465
Evaluate whether a rule should fire based on changed files.
434466
435467
Args:
436468
rule: Rule to evaluate
437469
changed_files: List of changed file paths (relative)
470+
created_files: List of newly created file paths (relative), for CREATED mode
438471
439472
Returns:
440473
RuleEvaluationResult with evaluation details
@@ -472,13 +505,28 @@ def evaluate_rule(rule: Rule, changed_files: list[str]) -> RuleEvaluationResult:
472505
missing_files=missing_files,
473506
)
474507

508+
elif rule.detection_mode == DetectionMode.CREATED:
509+
files_to_check = created_files if created_files is not None else []
510+
should_fire = evaluate_created(rule, files_to_check)
511+
trigger_files = (
512+
[f for f in files_to_check if matches_any_pattern(f, rule.created_patterns)]
513+
if should_fire
514+
else []
515+
)
516+
return RuleEvaluationResult(
517+
rule=rule,
518+
should_fire=should_fire,
519+
trigger_files=trigger_files,
520+
)
521+
475522
return RuleEvaluationResult(rule=rule, should_fire=False)
476523

477524

478525
def evaluate_rules(
479526
rules: list[Rule],
480527
changed_files: list[str],
481528
promised_rules: set[str] | None = None,
529+
created_files: list[str] | None = None,
482530
) -> list[RuleEvaluationResult]:
483531
"""
484532
Evaluate which rules should fire.
@@ -488,6 +536,7 @@ def evaluate_rules(
488536
changed_files: List of changed file paths (relative)
489537
promised_rules: Set of rule names that have been marked as addressed
490538
via <promise> tags (case-insensitive)
539+
created_files: List of newly created file paths (relative), for CREATED mode
491540
492541
Returns:
493542
List of RuleEvaluationResult for rules that should fire
@@ -504,7 +553,7 @@ def evaluate_rules(
504553
if rule.name.lower() in promised_lower:
505554
continue
506555

507-
result = evaluate_rule(rule, changed_files)
556+
result = evaluate_rule(rule, changed_files, created_files)
508557
if result.should_fire:
509558
results.append(result)
510559

0 commit comments

Comments
 (0)