Skip to content

Commit 6fbeb61

Browse files
authored
Merge branch 'main' into refactor/commands-to-skills
2 parents ff6f100 + 3de08e2 commit 6fbeb61

10 files changed

Lines changed: 305 additions & 5 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
name: "Manual Test: Infinite Block Command"
3+
trigger: manual_tests/test_infinite_block_command/test_infinite_block_command.py
4+
action:
5+
command: "false"
6+
run_for: each_match
7+
compare_to: prompt
8+
---
9+
10+
# Manual Test: Infinite Block Command (Promise Required)
11+
12+
This rule runs a command that ALWAYS FAILS (`false` returns exit code 1).
13+
14+
## Why this blocks
15+
16+
The command action always fails, creating an infinite block. The only way
17+
to proceed should be to provide a promise acknowledging that you understand
18+
the restriction.
19+
20+
## Expected behavior
21+
22+
If promises work correctly for command actions:
23+
- Without promise: Command runs, fails, blocks
24+
- With promise: Command is SKIPPED entirely, allows
25+
26+
If there's a bug:
27+
- The command will run and fail even when a promise is provided
28+
29+
## What to do
30+
31+
You MUST include the following promise tag in your response:
32+
33+
```
34+
<promise>Manual Test: Infinite Block Command</promise>
35+
```
36+
37+
## This tests
38+
39+
Whether the promise mechanism works for COMMAND-type rules. If a rule is
40+
promised, the command should not run at all - the rule should be skipped
41+
during evaluation.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
name: "Manual Test: Infinite Block Prompt"
3+
trigger: manual_tests/test_infinite_block_prompt/test_infinite_block_prompt.py
4+
compare_to: prompt
5+
---
6+
7+
# Manual Test: Infinite Block Prompt (Promise Required)
8+
9+
You edited `{trigger_files}` which triggers an infinite block.
10+
11+
## Why this blocks
12+
13+
This rule has NO safety file option and uses a PROMPT action. The only way
14+
to proceed is to provide a promise acknowledging that you understand the
15+
restriction.
16+
17+
## What to do
18+
19+
You MUST include the following promise tag in your response:
20+
21+
```
22+
<promise>Manual Test: Infinite Block Prompt</promise>
23+
```
24+
25+
This simulates scenarios where:
26+
- An operation requires explicit acknowledgment before proceeding
27+
- There is no alternative action that can suppress the rule
28+
- The agent must demonstrate understanding of the constraint
29+
30+
## This tests
31+
32+
The promise mechanism for PROMPT-type rules that cannot be satisfied by
33+
editing additional files. This is useful for enforcing policies where
34+
acknowledgment is the only valid response.

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [0.5.0] - 2026-01-18
99

10-
### Changed
1110
- **BREAKING**: Refactored "commands" terminology to "skills" throughout the codebase
1211
- Directory structure changed from `.claude/commands/` to `.claude/skills/`
1312
- Directory structure changed from `.gemini/commands/` to `.gemini/skills/`
@@ -27,6 +26,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2726
- Remove old `.claude/commands/` and `.gemini/commands/` directories manually
2827
- Update any custom code that imports `CommandGenerator` or `CommandLifecycleHook`
2928

29+
## [0.4.1] - 2026-01-18
30+
31+
### Fixed
32+
- Command rule errors now include promise skip instructions with the exact rule name
33+
- Previously, failed command rules only showed "Command failed" with no guidance
34+
- Now each failed rule shows: `To skip, include <promise>Rule Name</promise> in your response`
35+
- This allows agents to understand how to proceed when a command rule fails
36+
37+
3038
## [0.4.0] - 2026-01-16
3139

3240
### Added
@@ -106,6 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
106114

107115
Initial version.
108116

117+
[0.4.1]: https://github.com/anthropics/deepwork/releases/tag/0.4.1
109118
[0.4.0]: https://github.com/anthropics/deepwork/releases/tag/0.4.0
110119
[0.3.0]: https://github.com/anthropics/deepwork/releases/tag/0.3.0
111120
[0.1.1]: https://github.com/anthropics/deepwork/releases/tag/0.1.1

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,4 @@ For commercial use or questions about licensing, please contact legal@unsupervis
304304
## Credits
305305

306306
- Inspired by [GitHub's spec-kit](https://github.com/github/spec-kit)
307+

doc/architecture.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,3 +1270,4 @@ Claude: Created rule "API documentation update" in .deepwork/rules/api-documenta
12701270
- [Git Workflows](https://www.atlassian.com/git/tutorials/comparing-workflows)
12711271
- [JSON Schema](https://json-schema.org/)
12721272
- [Jinja2 Documentation](https://jinja.palletsprojects.com/)
1273+

manual_tests/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Each test has two cases: one where the rule SHOULD fire, and one where it should
3434
| **Pair Mode (reverse)** || Edit `_expected.md` only (should NOT fire) | Manual Test: Pair Mode |
3535
| **Command Action** | Edit `.txt` → log appended | — (always runs) | Manual Test: Command Action |
3636
| **Multi Safety** | Edit `.py` only | Edit `.py` AND any safety file | Manual Test: Multi Safety |
37+
| **Infinite Block Prompt** | Edit `.py` (always blocks) | Provide `<promise>` tag | Manual Test: Infinite Block Prompt |
38+
| **Infinite Block Command** | Edit `.py` (command fails) | Provide `<promise>` tag | Manual Test: Infinite Block Command |
3739

3840
## Test Results Tracking
3941

@@ -45,6 +47,8 @@ Each test has two cases: one where the rule SHOULD fire, and one where it should
4547
| Pair Mode (reverse - expected only) |||
4648
| Command Action |||
4749
| Multi Safety |||
50+
| Infinite Block Prompt |||
51+
| Infinite Block Command |||
4852

4953
## Test Folders
5054

@@ -55,6 +59,8 @@ Each test has two cases: one where the rule SHOULD fire, and one where it should
5559
| `test_pair_mode/` | Pair (Directional) | One-way: trigger requires expected, but not vice versa |
5660
| `test_command_action/` | Command Action | Automatically runs command on file change |
5761
| `test_multi_safety/` | Multiple Safety | Fires unless ANY of the safety files also edited |
62+
| `test_infinite_block_prompt/` | Infinite Block (Prompt) | Always blocks with prompt; only promise can bypass |
63+
| `test_infinite_block_command/` | Infinite Block (Command) | Command always fails; tests if promise skips command |
5864

5965
## Corresponding Rules
6066

@@ -64,3 +70,5 @@ Rules are defined in `.deepwork/rules/`:
6470
- `manual-test-pair-mode.md`
6571
- `manual-test-command-action.md`
6672
- `manual-test-multi-safety.md`
73+
- `manual-test-infinite-block-prompt.md`
74+
- `manual-test-infinite-block-command.md`
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
MANUAL TEST: Infinite Block Command Rule
3+
4+
=== WHAT THIS TESTS ===
5+
Tests a COMMAND-type rule with a command that ALWAYS FAILS - it will ALWAYS
6+
block when the trigger file is edited.
7+
8+
This verifies:
9+
1. The rule correctly blocks when the file is edited (command fails)
10+
2. The error output includes guidance on how to skip using a promise
11+
3. Without guidance in the output, the agent cannot know how to proceed
12+
13+
=== TEST CASE 1: Rule SHOULD fire (command fails, infinite block) ===
14+
1. Edit this file (add a comment below the marker)
15+
2. Run: echo '{}' | python -m deepwork.hooks.rules_check
16+
3. Expected: Block with command error AND promise skip instructions
17+
18+
=== TEST CASE 2: Rule should NOT fire (promise provided) ===
19+
1. Edit this file (add a comment below the marker)
20+
2. Provide a promise (format shown in command error output)
21+
3. Expected: Empty JSON {} (allow) - promise bypasses the command entirely
22+
23+
=== RULE LOCATION ===
24+
.deepwork/rules/manual-test-infinite-block-command.md
25+
26+
=== KEY DIFFERENCE FROM PROMPT VERSION ===
27+
- Prompt version: Shows instructions in the rule's markdown body
28+
- Command version: Must show instructions alongside command error output
29+
30+
If the command error output does NOT include promise skip instructions,
31+
this is a bug - the agent has no way to know how to proceed.
32+
"""
33+
34+
35+
def restricted_command_operation():
36+
"""An operation that requires explicit acknowledgment to proceed."""
37+
return "This operation uses a command that always fails"
38+
39+
40+
# Edit below this line to trigger the rule
41+
# -------------------------------------------
42+
# Test edit for command block
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
MANUAL TEST: Infinite Block Prompt Rule (Promise Required)
3+
4+
=== WHAT THIS TESTS ===
5+
Tests a PROMPT-type rule with NO safety file option - it will ALWAYS block
6+
when the trigger file is edited. The only way to proceed is to provide a
7+
promise in the correct format.
8+
9+
This verifies:
10+
1. The rule correctly blocks when the file is edited
11+
2. The promise mechanism works to bypass the block
12+
3. The promise must be in the exact format: <promise>Rule Name</promise>
13+
14+
=== TEST CASE 1: Rule SHOULD fire (infinite block) ===
15+
1. Edit this file (add a comment below the marker)
16+
2. Run: echo '{}' | python -m deepwork.hooks.rules_check
17+
3. Expected: "Manual Test: Infinite Block Prompt" appears in output with decision="block"
18+
4. The block message should explain that a promise is required
19+
20+
=== TEST CASE 2: Rule should NOT fire (promise provided) ===
21+
1. Edit this file (add a comment below the marker)
22+
2. Create a transcript with: <promise>Manual Test: Infinite Block Prompt</promise>
23+
3. Run the hook with the transcript
24+
4. Expected: Empty JSON {} (allow) - promise bypasses the block
25+
26+
=== HOW TO TEST WITH PROMISE ===
27+
The promise must be in the conversation transcript. To test:
28+
29+
1. Create a temp transcript file with the promise:
30+
echo '{"role":"assistant","message":{"content":[{"type":"text","text":"<promise>Manual Test: Infinite Block Prompt</promise>"}]}}' > /tmp/transcript.jsonl
31+
32+
2. Run with transcript:
33+
echo '{"transcript_path":"/tmp/transcript.jsonl"}' | python -m deepwork.hooks.rules_check
34+
35+
3. Expected: {} (empty JSON = allow)
36+
37+
=== RULE LOCATION ===
38+
.deepwork/rules/manual-test-infinite-block-prompt.md
39+
40+
=== KEY DIFFERENCE FROM OTHER TESTS ===
41+
Other tests have a "safety" file that can be edited to suppress the rule.
42+
This test has NO safety option - the ONLY way to proceed is with a promise.
43+
This simulates scenarios where the agent must explicitly acknowledge a
44+
constraint before proceeding.
45+
46+
=== COMPARISON WITH COMMAND VERSION ===
47+
See test_infinite_block_command/ for the command-action version of this test.
48+
"""
49+
50+
51+
def restricted_operation():
52+
"""An operation that requires explicit acknowledgment to proceed."""
53+
return "This operation always requires a promise to proceed"
54+
55+
56+
# Edit below this line to trigger the rule
57+
# -------------------------------------------

src/deepwork/hooks/rules_check.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,10 @@ def extract_promise_tags(text: str) -> set[str]:
244244
245245
Supports both:
246246
- <promise>Rule Name</promise>
247-
- <promise>Rule Name</promise>
247+
- <promise>Rule Name</promise>
248248
"""
249-
# Match with or without checkmark
250-
pattern = r"<promise>(?:\s*)?([^<]+)</promise>"
249+
# Match with optional checkmark prefix (✓ or ✓ with space)
250+
pattern = r"<promise>(?:\s*)?(?:✓\s*)?([^<]+)</promise>"
251251
matches = re.findall(pattern, text, re.IGNORECASE | re.DOTALL)
252252
return {m.strip() for m in matches}
253253

@@ -460,7 +460,8 @@ def rules_check_hook(hook_input: HookInput) -> HookOutput:
460460
else:
461461
# Command failed
462462
error_msg = format_command_errors(cmd_results)
463-
command_errors.append(f"## {rule.name}\n{error_msg}")
463+
skip_hint = f"To skip, include `<promise>✓ {rule.name}</promise>` in your response.\n"
464+
command_errors.append(f"## {rule.name}\n{error_msg}{skip_hint}")
464465
queue.update_status(
465466
trigger_hash,
466467
QueueEntryStatus.FAILED,
@@ -481,6 +482,7 @@ def rules_check_hook(hook_input: HookInput) -> HookOutput:
481482
# Add command errors if any
482483
if command_errors:
483484
messages.append("## Command Rule Errors\n")
485+
messages.append("The following command rules failed.\n")
484486
messages.extend(command_errors)
485487
messages.append("")
486488

tests/unit/test_rules_check.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Tests for rules_check hook module."""
2+
3+
from deepwork.hooks.rules_check import extract_promise_tags
4+
5+
6+
class TestExtractPromiseTags:
7+
"""Tests for extract_promise_tags function."""
8+
9+
def test_extracts_simple_promise(self) -> None:
10+
"""Test extracting a simple promise tag."""
11+
text = "I've reviewed this. <promise>Rule Name</promise>"
12+
result = extract_promise_tags(text)
13+
assert result == {"Rule Name"}
14+
15+
def test_extracts_promise_with_checkmark(self) -> None:
16+
"""Test extracting promise tag with checkmark prefix."""
17+
text = "Done. <promise>✓ Rule Name</promise>"
18+
result = extract_promise_tags(text)
19+
assert result == {"Rule Name"}
20+
21+
def test_extracts_promise_with_checkmark_no_space(self) -> None:
22+
"""Test extracting promise tag with checkmark but no space."""
23+
text = "<promise>✓Rule Name</promise>"
24+
result = extract_promise_tags(text)
25+
assert result == {"Rule Name"}
26+
27+
def test_extracts_multiple_promises(self) -> None:
28+
"""Test extracting multiple promise tags."""
29+
text = """
30+
<promise>Rule One</promise>
31+
<promise>✓ Rule Two</promise>
32+
<promise>Rule Three</promise>
33+
"""
34+
result = extract_promise_tags(text)
35+
assert result == {"Rule One", "Rule Two", "Rule Three"}
36+
37+
def test_case_insensitive_tag(self) -> None:
38+
"""Test that promise tags are case-insensitive."""
39+
text = "<PROMISE>Rule Name</PROMISE>"
40+
result = extract_promise_tags(text)
41+
assert result == {"Rule Name"}
42+
43+
def test_preserves_rule_name_case(self) -> None:
44+
"""Test that rule name case is preserved."""
45+
text = "<promise>Architecture Documentation Accuracy</promise>"
46+
result = extract_promise_tags(text)
47+
assert result == {"Architecture Documentation Accuracy"}
48+
49+
def test_handles_whitespace_in_tag(self) -> None:
50+
"""Test handling of whitespace around rule name."""
51+
text = "<promise> Rule Name </promise>"
52+
result = extract_promise_tags(text)
53+
assert result == {"Rule Name"}
54+
55+
def test_handles_newlines_in_tag(self) -> None:
56+
"""Test handling of newlines in promise tag."""
57+
text = "<promise>\n Rule Name\n</promise>"
58+
result = extract_promise_tags(text)
59+
assert result == {"Rule Name"}
60+
61+
def test_returns_empty_set_for_no_promises(self) -> None:
62+
"""Test that empty set is returned when no promises exist."""
63+
text = "No promises here."
64+
result = extract_promise_tags(text)
65+
assert result == set()
66+
67+
def test_handles_empty_string(self) -> None:
68+
"""Test handling of empty string."""
69+
result = extract_promise_tags("")
70+
assert result == set()
71+
72+
def test_real_world_command_error_promise(self) -> None:
73+
"""Test promise format shown in command error output."""
74+
# This is the exact format shown to agents when a command rule fails
75+
text = "<promise>✓ Manual Test: Infinite Block Command</promise>"
76+
result = extract_promise_tags(text)
77+
assert result == {"Manual Test: Infinite Block Command"}
78+
79+
def test_mixed_formats_in_same_text(self) -> None:
80+
"""Test extracting both checkmark and non-checkmark promises."""
81+
text = """
82+
<promise>Rule Without Checkmark</promise>
83+
<promise>✓ Rule With Checkmark</promise>
84+
"""
85+
result = extract_promise_tags(text)
86+
assert result == {"Rule Without Checkmark", "Rule With Checkmark"}
87+
88+
def test_promise_with_special_characters_in_name(self) -> None:
89+
"""Test promise with special characters in rule name."""
90+
text = "<promise>Source/Test Pairing</promise>"
91+
result = extract_promise_tags(text)
92+
assert result == {"Source/Test Pairing"}
93+
94+
def test_promise_embedded_in_markdown(self) -> None:
95+
"""Test promise tag embedded in markdown text."""
96+
text = """
97+
I've reviewed the documentation and it's accurate.
98+
99+
<promise>Architecture Documentation Accuracy</promise>
100+
<promise>README Accuracy</promise>
101+
102+
The changes were purely cosmetic.
103+
"""
104+
result = extract_promise_tags(text)
105+
assert result == {"Architecture Documentation Accuracy", "README Accuracy"}

0 commit comments

Comments
 (0)