Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .deepwork/rules/new-standard-job-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: New Standard Job Warning
created: src/deepwork/standard_jobs/*/job.yml
compare_to: prompt
---
A new standard job is being created. Standard jobs are bundled with DeepWork and will be installed in any project that uses DeepWork.

**Before proceeding, verify this is intentional:**

- **Standard jobs** (`src/deepwork/standard_jobs/`) - Ship with DeepWork, auto-installed in all projects that use DeepWork
- **Repository jobs** (`.deepwork/jobs/`) - Specific to a single repository
- **Library jobs** - Installed from external packages

Unless the user **explicitly requested** creating a new standard job (not just "a job" or "a new job"), this should likely be a **repository job** in `.deepwork/jobs/` instead.

If uncertain, ask the user: "Should this be a standard job (shipped with DeepWork) or a repository-specific job?"
108 changes: 107 additions & 1 deletion doc/rules_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ This rule runs `ruff format` on any changed Python files to ensure
consistent code style across the codebase.
```

### Created Mode (file creation trigger)

`.deepwork/rules/new-module-docs.md`:
```markdown
---
name: New Module Documentation
created: src/**/*.py
---
A new Python module was created. Please ensure:

- Add module docstring explaining the purpose
- Update relevant documentation if adding a public API
- Consider adding tests for the new module
```

## Rule Structure

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

### Action Type

Expand Down Expand Up @@ -206,6 +222,47 @@ If `api/users/create.py` changes:
If `docs/api/users/create.md` changes alone:
- No trigger (documentation can be updated independently)

### Created Mode (File Creation Detection)

Fires only when files are newly created (not modified). Useful for enforcing standards on new files.

```yaml
---
name: New Component Documentation
created:
- src/components/**/*.tsx
- src/components/**/*.ts
---
```

**How it works:**

1. A file is created that matches a `created` pattern
2. Rule fires with instructions

Key differences from Trigger/Safety mode:
- Only fires for **new** files, not modifications to existing files
- No safety patterns (use Trigger/Safety mode if you need safety)
- Good for enforcing documentation, tests, or standards on new code

**Examples:**

```yaml
# Single pattern
created: src/api/**/*.py

# Multiple patterns
created:
- src/models/**/*.py
- src/services/**/*.py
```

If a new file `src/api/users.py` is created:
- Rule fires with instructions for new API modules

If an existing file `src/api/users.py` is modified:
- Rule does NOT fire (file already existed)

## Action Types

### Prompt Action (Default)
Expand Down Expand Up @@ -382,6 +439,22 @@ pair:
---
```

### created

File patterns that trigger when files are newly created (created mode). Only fires for new files, not modifications. Can be string or array.

```yaml
---
created: src/**/*.py
---

---
created:
- src/**/*.py
- lib/**/*.py
---
```

### action (optional)

Specifies a command to run instead of prompting.
Expand Down Expand Up @@ -517,6 +590,39 @@ This rule is suppressed if you've already modified pyproject.toml
or CHANGELOG.md, as that indicates you're handling versioning.
```

### Example 6: New File Standards (Created Mode)

`.deepwork/rules/new-module-standards.md`:
```markdown
---
name: New Module Standards
created:
- src/**/*.py
- lib/**/*.py
---
A new Python module was created. Please ensure it follows our standards:

1. **Module docstring**: Add a docstring at the top explaining the module's purpose
2. **Type hints**: Use type hints for all function parameters and return values
3. **Tests**: Create a corresponding test file in tests/
4. **Imports**: Follow the import order (stdlib, third-party, local)

This rule only fires for newly created files, not modifications.
```

### Example 7: New Component Checklist (Created Mode with Command)

`.deepwork/rules/new-component-lint.md`:
```markdown
---
name: New Component Lint
created: src/components/**/*.tsx
action:
command: eslint --fix {file}
---
Automatically lints newly created React components.
```

## Promise Tags

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:
Expand All @@ -539,7 +645,7 @@ Error: .deepwork/rules/my-rule.md - invalid YAML frontmatter

**Missing required field:**
```
Error: .deepwork/rules/my-rule.md - must have 'trigger', 'set', or 'pair'
Error: .deepwork/rules/my-rule.md - must have 'trigger', 'set', 'pair', or 'created'
```

**Invalid pattern:**
Expand Down
22 changes: 22 additions & 0 deletions doc/rules_system_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Every rule has two orthogonal aspects:
| **Trigger/Safety** | `trigger`, `safety` | Fire when trigger matches and safety doesn't |
| **Set** | `set` | Fire when file correspondence is incomplete (bidirectional) |
| **Pair** | `pair` | Fire when file correspondence is incomplete (directional) |
| **Created** | `created` | Fire when newly created files match patterns |

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

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

**Created Mode (File Creation Detection)**
- Define patterns for newly created files
- Only fires when files are created, not when existing files are modified
- Useful for enforcing standards on new code (documentation, tests, etc.)
- Example: New modules require documentation and tests

### Pattern Variables

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

### Created Rule

```
1. Detector: New file created, matches "src/**/*.py" created pattern
2. Detector: Verify file is newly created (not just modified)
3. Detector: Create .queued entry for new file rule
4. Evaluator: Return instructions for new file standards
5. Agent: Addresses rule, includes <promise> tag
6. Evaluator: On next check, mark .passed (promise found)
```

Note: Created mode uses separate file detection to distinguish newly
created files from modified files. Untracked files and files added
since the baseline are considered "created".

## Agent Output Management

### Problem
Expand Down
55 changes: 51 additions & 4 deletions src/deepwork/core/rules_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class DetectionMode(Enum):
TRIGGER_SAFETY = "trigger_safety" # Fire when trigger matches, safety doesn't
SET = "set" # Bidirectional file correspondence
PAIR = "pair" # Directional file correspondence
CREATED = "created" # Fire when created files match patterns


class ActionType(Enum):
Expand Down Expand Up @@ -77,6 +78,7 @@ class Rule:
safety: list[str] = field(default_factory=list) # For TRIGGER_SAFETY mode
set_patterns: list[str] = field(default_factory=list) # For SET mode
pair_config: PairConfig | None = None # For PAIR mode
created_patterns: list[str] = field(default_factory=list) # For CREATED mode

# Action type
action_type: ActionType = ActionType.PROMPT
Expand Down Expand Up @@ -113,10 +115,11 @@ def from_frontmatter(
has_trigger = "trigger" in frontmatter
has_set = "set" in frontmatter
has_pair = "pair" in frontmatter
has_created = "created" in frontmatter

mode_count = sum([has_trigger, has_set, has_pair])
mode_count = sum([has_trigger, has_set, has_pair, has_created])
if mode_count == 0:
raise RulesParseError(f"Rule '{name}' must have 'trigger', 'set', or 'pair'")
raise RulesParseError(f"Rule '{name}' must have 'trigger', 'set', 'pair', or 'created'")
if mode_count > 1:
raise RulesParseError(f"Rule '{name}' has multiple detection modes - use only one")

Expand All @@ -126,6 +129,7 @@ def from_frontmatter(
safety: list[str] = []
set_patterns: list[str] = []
pair_config: PairConfig | None = None
created_patterns: list[str] = []

if has_trigger:
detection_mode = DetectionMode.TRIGGER_SAFETY
Expand All @@ -150,6 +154,11 @@ def from_frontmatter(
expects=expects_list,
)

elif has_created:
detection_mode = DetectionMode.CREATED
created = frontmatter["created"]
created_patterns = [created] if isinstance(created, str) else list(created)

# Determine action type
action_type: ActionType
command_action: CommandAction | None = None
Expand Down Expand Up @@ -178,6 +187,7 @@ def from_frontmatter(
safety=safety,
set_patterns=set_patterns,
pair_config=pair_config,
created_patterns=created_patterns,
action_type=action_type,
instructions=markdown_body.strip(),
command_action=command_action,
Expand Down Expand Up @@ -419,6 +429,22 @@ def evaluate_pair_correspondence(
return should_fire, trigger_files, missing_files


def evaluate_created(
rule: Rule,
created_files: list[str],
) -> bool:
"""
Evaluate a created mode rule.

Returns True if rule should fire:
- At least one created file matches a created pattern
"""
for file_path in created_files:
if matches_any_pattern(file_path, rule.created_patterns):
return True
return False


@dataclass
class RuleEvaluationResult:
"""Result of evaluating a single rule."""
Expand All @@ -429,13 +455,18 @@ class RuleEvaluationResult:
missing_files: list[str] = field(default_factory=list) # For set/pair modes


def evaluate_rule(rule: Rule, changed_files: list[str]) -> RuleEvaluationResult:
def evaluate_rule(
rule: Rule,
changed_files: list[str],
created_files: list[str] | None = None,
) -> RuleEvaluationResult:
"""
Evaluate whether a rule should fire based on changed files.

Args:
rule: Rule to evaluate
changed_files: List of changed file paths (relative)
created_files: List of newly created file paths (relative), for CREATED mode

Returns:
RuleEvaluationResult with evaluation details
Expand Down Expand Up @@ -473,13 +504,28 @@ def evaluate_rule(rule: Rule, changed_files: list[str]) -> RuleEvaluationResult:
missing_files=missing_files,
)

elif rule.detection_mode == DetectionMode.CREATED:
files_to_check = created_files if created_files is not None else []
should_fire = evaluate_created(rule, files_to_check)
trigger_files = (
[f for f in files_to_check if matches_any_pattern(f, rule.created_patterns)]
if should_fire
else []
)
return RuleEvaluationResult(
rule=rule,
should_fire=should_fire,
trigger_files=trigger_files,
)

return RuleEvaluationResult(rule=rule, should_fire=False)


def evaluate_rules(
rules: list[Rule],
changed_files: list[str],
promised_rules: set[str] | None = None,
created_files: list[str] | None = None,
) -> list[RuleEvaluationResult]:
"""
Evaluate which rules should fire.
Expand All @@ -489,6 +535,7 @@ def evaluate_rules(
changed_files: List of changed file paths (relative)
promised_rules: Set of rule names that have been marked as addressed
via <promise> tags (case-insensitive)
created_files: List of newly created file paths (relative), for CREATED mode

Returns:
List of RuleEvaluationResult for rules that should fire
Expand All @@ -505,7 +552,7 @@ def evaluate_rules(
if rule.name.lower() in promised_lower:
continue

result = evaluate_rule(rule, changed_files)
result = evaluate_rule(rule, changed_files, created_files)
if result.should_fire:
results.append(result)

Expand Down
Loading