deepwork 0.3.1__py3-none-any.whl → 0.5.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/hook.py +70 -0
- deepwork/cli/install.py +58 -14
- deepwork/cli/main.py +4 -0
- deepwork/cli/rules.py +32 -0
- deepwork/cli/sync.py +27 -1
- deepwork/core/adapters.py +213 -0
- deepwork/core/command_executor.py +26 -9
- deepwork/core/doc_spec_parser.py +205 -0
- deepwork/core/generator.py +79 -4
- deepwork/core/hooks_syncer.py +15 -2
- deepwork/core/parser.py +64 -2
- deepwork/hooks/__init__.py +9 -3
- deepwork/hooks/check_version.sh +230 -0
- deepwork/hooks/claude_hook.sh +13 -17
- deepwork/hooks/gemini_hook.sh +13 -17
- deepwork/hooks/rules_check.py +71 -12
- deepwork/hooks/wrapper.py +66 -16
- deepwork/schemas/doc_spec_schema.py +64 -0
- deepwork/schemas/job_schema.py +25 -3
- deepwork/standard_jobs/deepwork_jobs/doc_specs/job_spec.md +190 -0
- deepwork/standard_jobs/deepwork_jobs/job.yml +41 -8
- deepwork/standard_jobs/deepwork_jobs/steps/define.md +68 -2
- deepwork/standard_jobs/deepwork_jobs/steps/implement.md +3 -3
- deepwork/standard_jobs/deepwork_jobs/steps/learn.md +74 -5
- deepwork/standard_jobs/deepwork_jobs/steps/review_job_spec.md +208 -0
- deepwork/standard_jobs/deepwork_jobs/templates/doc_spec.md.example +86 -0
- deepwork/standard_jobs/deepwork_jobs/templates/doc_spec.md.template +26 -0
- deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +8 -0
- deepwork/standard_jobs/deepwork_rules/job.yml +5 -3
- deepwork/templates/claude/AGENTS.md +38 -0
- deepwork/templates/claude/skill-job-meta.md.jinja +7 -0
- deepwork/templates/claude/skill-job-step.md.jinja +107 -70
- deepwork/templates/gemini/skill-job-step.toml.jinja +18 -3
- deepwork/utils/fs.py +36 -0
- deepwork/utils/yaml_utils.py +24 -0
- {deepwork-0.3.1.dist-info → deepwork-0.5.1.dist-info}/METADATA +39 -2
- deepwork-0.5.1.dist-info/RECORD +72 -0
- deepwork-0.3.1.dist-info/RECORD +0 -62
- {deepwork-0.3.1.dist-info → deepwork-0.5.1.dist-info}/WHEEL +0 -0
- {deepwork-0.3.1.dist-info → deepwork-0.5.1.dist-info}/entry_points.txt +0 -0
- {deepwork-0.3.1.dist-info → deepwork-0.5.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check_version.sh - SessionStart hook to check Claude Code version and deepwork installation
|
|
3
|
+
#
|
|
4
|
+
# This hook performs two critical checks:
|
|
5
|
+
# 1. Verifies that the 'deepwork' command is installed and directly invokable
|
|
6
|
+
# 2. Warns users if their Claude Code version is below the minimum required
|
|
7
|
+
#
|
|
8
|
+
# The deepwork check is blocking (exit 2) because hooks cannot function without it.
|
|
9
|
+
# The version check is informational only (exit 0) to avoid blocking sessions.
|
|
10
|
+
#
|
|
11
|
+
# Uses hookSpecificOutput.additionalContext to pass messages to Claude's context.
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# DEEPWORK INSTALLATION CHECK (BLOCKING)
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# This check runs on EVERY hook invocation (no re-entry guard) because if
|
|
17
|
+
# deepwork is not installed, nothing else will work.
|
|
18
|
+
|
|
19
|
+
check_deepwork_installed() {
|
|
20
|
+
# Run 'deepwork rules clear_queue' instead of just '--version' for double utility:
|
|
21
|
+
# 1. Verifies that the 'deepwork' command is installed and directly invokable
|
|
22
|
+
# 2. Clears any stale rules from the queue, ensuring a clean slate for the session
|
|
23
|
+
if ! deepwork rules clear_queue >/dev/null 2>&1; then
|
|
24
|
+
return 1
|
|
25
|
+
fi
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
print_deepwork_error() {
|
|
30
|
+
cat >&2 << 'EOF'
|
|
31
|
+
|
|
32
|
+
================================================================================
|
|
33
|
+
*** DEEPWORK NOT INSTALLED ***
|
|
34
|
+
================================================================================
|
|
35
|
+
|
|
36
|
+
ERROR: The 'deepwork' command is not available or cannot be directly invoked.
|
|
37
|
+
|
|
38
|
+
DeepWork must be installed such that running 'deepwork' directly works.
|
|
39
|
+
For example, running 'deepwork rules clear_queue' should succeed.
|
|
40
|
+
|
|
41
|
+
IMPORTANT: Do NOT use 'uv run deepwork' or similar wrappers.
|
|
42
|
+
The command must be directly invokable as just 'deepwork'.
|
|
43
|
+
|
|
44
|
+
------------------------------------------------------------------------
|
|
45
|
+
| |
|
|
46
|
+
| Please fix your deepwork installation before proceeding. |
|
|
47
|
+
| |
|
|
48
|
+
| Installation options: |
|
|
49
|
+
| - pipx install deepwork |
|
|
50
|
+
| - pip install --user deepwork (ensure ~/.local/bin is in PATH) |
|
|
51
|
+
| - nix develop (if using the nix flake) |
|
|
52
|
+
| |
|
|
53
|
+
------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
================================================================================
|
|
56
|
+
|
|
57
|
+
EOF
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
output_deepwork_error_json() {
|
|
61
|
+
cat << 'EOF'
|
|
62
|
+
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"DEEPWORK INSTALLATION ERROR: The 'deepwork' command is not installed or cannot be directly invoked. DeepWork must be installed such that it can be directly invoked (e.g., 'deepwork', NOT 'uv run deepwork'). Please fix your deepwork installation before proceeding with anything else. DO NOT CONTINUE until this is resolved."},"error":"deepwork command not found - please install deepwork so it can be directly invoked"}
|
|
63
|
+
EOF
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Check deepwork installation FIRST (before any other checks)
|
|
67
|
+
if ! check_deepwork_installed; then
|
|
68
|
+
print_deepwork_error
|
|
69
|
+
output_deepwork_error_json
|
|
70
|
+
exit 2 # Blocking error - prevent session from continuing
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# ============================================================================
|
|
74
|
+
# RE-ENTRY GUARD (for version check only)
|
|
75
|
+
# ============================================================================
|
|
76
|
+
# SessionStart hooks can be triggered multiple times in a session (on resume,
|
|
77
|
+
# clear, etc.). We only want to show the version warning once per session to
|
|
78
|
+
# avoid spamming the user. We use an environment variable to track whether
|
|
79
|
+
# we've already run. Note: This relies on the parent process preserving env
|
|
80
|
+
# vars across hook invocations within the same session.
|
|
81
|
+
if [ -n "$DEEPWORK_VERSION_CHECK_DONE" ]; then
|
|
82
|
+
# Already checked version this session, exit silently with empty JSON
|
|
83
|
+
echo '{}'
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
export DEEPWORK_VERSION_CHECK_DONE=1
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# MINIMUM VERSION CONFIGURATION
|
|
90
|
+
# ============================================================================
|
|
91
|
+
MINIMUM_VERSION="2.1.14"
|
|
92
|
+
|
|
93
|
+
# ============================================================================
|
|
94
|
+
# VERSION CHECK LOGIC
|
|
95
|
+
# ============================================================================
|
|
96
|
+
|
|
97
|
+
# Get current Claude Code version
|
|
98
|
+
get_current_version() {
|
|
99
|
+
local version_output
|
|
100
|
+
version_output=$(claude --version 2>/dev/null) || return 1
|
|
101
|
+
# Extract version number (e.g., "2.1.1" from "2.1.1 (Claude Code)")
|
|
102
|
+
echo "$version_output" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+' | head -1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Compare two semantic versions
|
|
106
|
+
# Returns 0 if version1 >= version2, 1 otherwise
|
|
107
|
+
version_gte() {
|
|
108
|
+
local version1="$1"
|
|
109
|
+
local version2="$2"
|
|
110
|
+
|
|
111
|
+
# Split versions into components
|
|
112
|
+
local v1_major v1_minor v1_patch
|
|
113
|
+
local v2_major v2_minor v2_patch
|
|
114
|
+
|
|
115
|
+
IFS='.' read -r v1_major v1_minor v1_patch <<< "$version1"
|
|
116
|
+
IFS='.' read -r v2_major v2_minor v2_patch <<< "$version2"
|
|
117
|
+
|
|
118
|
+
# Default to 0 if component is missing
|
|
119
|
+
v1_major=${v1_major:-0}
|
|
120
|
+
v1_minor=${v1_minor:-0}
|
|
121
|
+
v1_patch=${v1_patch:-0}
|
|
122
|
+
v2_major=${v2_major:-0}
|
|
123
|
+
v2_minor=${v2_minor:-0}
|
|
124
|
+
v2_patch=${v2_patch:-0}
|
|
125
|
+
|
|
126
|
+
# Compare major version
|
|
127
|
+
if [ "$v1_major" -gt "$v2_major" ]; then
|
|
128
|
+
return 0
|
|
129
|
+
elif [ "$v1_major" -lt "$v2_major" ]; then
|
|
130
|
+
return 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Compare minor version
|
|
134
|
+
if [ "$v1_minor" -gt "$v2_minor" ]; then
|
|
135
|
+
return 0
|
|
136
|
+
elif [ "$v1_minor" -lt "$v2_minor" ]; then
|
|
137
|
+
return 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Compare patch version
|
|
141
|
+
if [ "$v1_patch" -ge "$v2_patch" ]; then
|
|
142
|
+
return 0
|
|
143
|
+
else
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Generate warning message
|
|
149
|
+
get_warning_message() {
|
|
150
|
+
local current_version="$1"
|
|
151
|
+
|
|
152
|
+
cat << EOF
|
|
153
|
+
CLAUDE CODE VERSION WARNING: Your version (${current_version}) is below the minimum recommended (${MINIMUM_VERSION}). Older versions have known bugs that may cause issues with DeepWork. RECOMMENDED: Update your version of Claude Code. TELL THE USER ABOUT THIS ISSUE LOUDLY!!!
|
|
154
|
+
EOF
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Print visual warning to stderr for immediate user visibility
|
|
158
|
+
print_stderr_warning() {
|
|
159
|
+
local current_version="$1"
|
|
160
|
+
|
|
161
|
+
cat >&2 << EOF
|
|
162
|
+
|
|
163
|
+
================================================================================
|
|
164
|
+
*** CLAUDE CODE VERSION WARNING ***
|
|
165
|
+
================================================================================
|
|
166
|
+
|
|
167
|
+
Your Claude Code version: ${current_version}
|
|
168
|
+
Minimum recommended: ${MINIMUM_VERSION}
|
|
169
|
+
|
|
170
|
+
IMPORTANT: Versions below the minimum have known bugs that may cause
|
|
171
|
+
issues with DeepWork functionality. You may experience unexpected
|
|
172
|
+
behavior, errors, or incomplete operations.
|
|
173
|
+
|
|
174
|
+
------------------------------------------------------------------------
|
|
175
|
+
| |
|
|
176
|
+
| RECOMMENDED ACTION: Update your version of Claude Code |
|
|
177
|
+
| |
|
|
178
|
+
------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
================================================================================
|
|
181
|
+
|
|
182
|
+
EOF
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Output JSON with additional context for Claude
|
|
186
|
+
output_json_with_context() {
|
|
187
|
+
local context="$1"
|
|
188
|
+
# Escape special characters for JSON
|
|
189
|
+
local escaped_context
|
|
190
|
+
escaped_context=$(echo "$context" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ')
|
|
191
|
+
|
|
192
|
+
cat << EOF
|
|
193
|
+
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"${escaped_context}"}}
|
|
194
|
+
EOF
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# ============================================================================
|
|
198
|
+
# MAIN
|
|
199
|
+
# ============================================================================
|
|
200
|
+
|
|
201
|
+
main() {
|
|
202
|
+
local current_version
|
|
203
|
+
local warning_message
|
|
204
|
+
|
|
205
|
+
# Get current version (don't exit on failure)
|
|
206
|
+
current_version=$(get_current_version) || current_version=""
|
|
207
|
+
|
|
208
|
+
if [ -z "$current_version" ]; then
|
|
209
|
+
# Could not determine version, output empty JSON and exit
|
|
210
|
+
echo '{}'
|
|
211
|
+
exit 0
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# Check if current version is below minimum
|
|
215
|
+
if ! version_gte "$current_version" "$MINIMUM_VERSION"; then
|
|
216
|
+
# Print visual warning to stderr
|
|
217
|
+
print_stderr_warning "$current_version"
|
|
218
|
+
|
|
219
|
+
# Output JSON with context for Claude
|
|
220
|
+
warning_message=$(get_warning_message "$current_version")
|
|
221
|
+
output_json_with_context "$warning_message"
|
|
222
|
+
else
|
|
223
|
+
# Version is OK, output empty JSON
|
|
224
|
+
echo '{}'
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
exit 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main "$@"
|
deepwork/hooks/claude_hook.sh
CHANGED
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
# and work on any supported platform.
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
# claude_hook.sh <
|
|
9
|
+
# claude_hook.sh <hook_name>
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# claude_hook.sh
|
|
12
|
+
# claude_hook.sh rules_check
|
|
13
13
|
#
|
|
14
|
-
# The
|
|
15
|
-
#
|
|
16
|
-
# 2. The hook function receives HookInput and returns HookOutput
|
|
14
|
+
# The hook is run via the deepwork CLI, which works regardless of how
|
|
15
|
+
# deepwork was installed (pipx, uv, nix flake, etc.).
|
|
17
16
|
#
|
|
18
17
|
# Environment variables set by Claude Code:
|
|
19
18
|
# CLAUDE_PROJECT_DIR - Absolute path to project root
|
|
@@ -26,12 +25,12 @@
|
|
|
26
25
|
|
|
27
26
|
set -e
|
|
28
27
|
|
|
29
|
-
# Get the
|
|
30
|
-
|
|
28
|
+
# Get the hook name to run
|
|
29
|
+
HOOK_NAME="${1:-}"
|
|
31
30
|
|
|
32
|
-
if [ -z "${
|
|
33
|
-
echo "Usage: claude_hook.sh <
|
|
34
|
-
echo "Example: claude_hook.sh
|
|
31
|
+
if [ -z "${HOOK_NAME}" ]; then
|
|
32
|
+
echo "Usage: claude_hook.sh <hook_name>" >&2
|
|
33
|
+
echo "Example: claude_hook.sh rules_check" >&2
|
|
35
34
|
exit 1
|
|
36
35
|
fi
|
|
37
36
|
|
|
@@ -41,15 +40,12 @@ if [ ! -t 0 ]; then
|
|
|
41
40
|
HOOK_INPUT=$(cat)
|
|
42
41
|
fi
|
|
43
42
|
|
|
44
|
-
# Set platform environment variable for the
|
|
43
|
+
# Set platform environment variable for the hook
|
|
45
44
|
export DEEPWORK_HOOK_PLATFORM="claude"
|
|
46
45
|
|
|
47
|
-
# Run the
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
# 2. Processing the hook logic
|
|
51
|
-
# 3. Writing JSON to stdout
|
|
52
|
-
echo "${HOOK_INPUT}" | python -m "${PYTHON_MODULE}"
|
|
46
|
+
# Run the hook via deepwork CLI
|
|
47
|
+
# This works regardless of how deepwork was installed (pipx, uv, nix flake, etc.)
|
|
48
|
+
echo "${HOOK_INPUT}" | deepwork hook "${HOOK_NAME}"
|
|
53
49
|
exit_code=$?
|
|
54
50
|
|
|
55
51
|
exit ${exit_code}
|
deepwork/hooks/gemini_hook.sh
CHANGED
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
# and work on any supported platform.
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
# gemini_hook.sh <
|
|
9
|
+
# gemini_hook.sh <hook_name>
|
|
10
10
|
#
|
|
11
11
|
# Example:
|
|
12
|
-
# gemini_hook.sh
|
|
12
|
+
# gemini_hook.sh rules_check
|
|
13
13
|
#
|
|
14
|
-
# The
|
|
15
|
-
#
|
|
16
|
-
# 2. The hook function receives HookInput and returns HookOutput
|
|
14
|
+
# The hook is run via the deepwork CLI, which works regardless of how
|
|
15
|
+
# deepwork was installed (pipx, uv, nix flake, etc.).
|
|
17
16
|
#
|
|
18
17
|
# Environment variables set by Gemini CLI:
|
|
19
18
|
# GEMINI_PROJECT_DIR - Absolute path to project root
|
|
@@ -26,12 +25,12 @@
|
|
|
26
25
|
|
|
27
26
|
set -e
|
|
28
27
|
|
|
29
|
-
# Get the
|
|
30
|
-
|
|
28
|
+
# Get the hook name to run
|
|
29
|
+
HOOK_NAME="${1:-}"
|
|
31
30
|
|
|
32
|
-
if [ -z "${
|
|
33
|
-
echo "Usage: gemini_hook.sh <
|
|
34
|
-
echo "Example: gemini_hook.sh
|
|
31
|
+
if [ -z "${HOOK_NAME}" ]; then
|
|
32
|
+
echo "Usage: gemini_hook.sh <hook_name>" >&2
|
|
33
|
+
echo "Example: gemini_hook.sh rules_check" >&2
|
|
35
34
|
exit 1
|
|
36
35
|
fi
|
|
37
36
|
|
|
@@ -41,15 +40,12 @@ if [ ! -t 0 ]; then
|
|
|
41
40
|
HOOK_INPUT=$(cat)
|
|
42
41
|
fi
|
|
43
42
|
|
|
44
|
-
# Set platform environment variable for the
|
|
43
|
+
# Set platform environment variable for the hook
|
|
45
44
|
export DEEPWORK_HOOK_PLATFORM="gemini"
|
|
46
45
|
|
|
47
|
-
# Run the
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
# 2. Processing the hook logic
|
|
51
|
-
# 3. Writing JSON to stdout
|
|
52
|
-
echo "${HOOK_INPUT}" | python -m "${PYTHON_MODULE}"
|
|
46
|
+
# Run the hook via deepwork CLI
|
|
47
|
+
# This works regardless of how deepwork was installed (pipx, uv, nix flake, etc.)
|
|
48
|
+
echo "${HOOK_INPUT}" | deepwork hook "${HOOK_NAME}"
|
|
53
49
|
exit_code=$?
|
|
54
50
|
|
|
55
51
|
exit ${exit_code}
|
deepwork/hooks/rules_check.py
CHANGED
|
@@ -6,12 +6,15 @@ It uses the wrapper system for cross-platform compatibility.
|
|
|
6
6
|
|
|
7
7
|
Rule files are loaded from .deepwork/rules/ directory as frontmatter markdown files.
|
|
8
8
|
|
|
9
|
-
Usage (via shell wrapper):
|
|
10
|
-
claude_hook.sh
|
|
11
|
-
gemini_hook.sh
|
|
9
|
+
Usage (via shell wrapper - recommended):
|
|
10
|
+
claude_hook.sh rules_check
|
|
11
|
+
gemini_hook.sh rules_check
|
|
12
12
|
|
|
13
|
-
Or directly
|
|
14
|
-
|
|
13
|
+
Or directly via deepwork CLI:
|
|
14
|
+
deepwork hook rules_check
|
|
15
|
+
|
|
16
|
+
Or with platform environment variable:
|
|
17
|
+
DEEPWORK_HOOK_PLATFORM=claude deepwork hook rules_check
|
|
15
18
|
"""
|
|
16
19
|
|
|
17
20
|
from __future__ import annotations
|
|
@@ -611,6 +614,26 @@ def rules_check_hook(hook_input: HookInput) -> HookOutput:
|
|
|
611
614
|
):
|
|
612
615
|
continue
|
|
613
616
|
|
|
617
|
+
# For PROMPT rules, also skip if already QUEUED (already shown to agent).
|
|
618
|
+
# This prevents infinite loops when transcript is unavailable or promise
|
|
619
|
+
# tags haven't been written yet. The agent has already seen this rule.
|
|
620
|
+
if (
|
|
621
|
+
existing
|
|
622
|
+
and existing.status == QueueEntryStatus.QUEUED
|
|
623
|
+
and rule.action_type == ActionType.PROMPT
|
|
624
|
+
):
|
|
625
|
+
continue
|
|
626
|
+
|
|
627
|
+
# For COMMAND rules with FAILED status, don't re-run the command.
|
|
628
|
+
# The agent has already seen the error. If they provide a promise,
|
|
629
|
+
# the after-loop logic will update the status to SKIPPED.
|
|
630
|
+
if (
|
|
631
|
+
existing
|
|
632
|
+
and existing.status == QueueEntryStatus.FAILED
|
|
633
|
+
and rule.action_type == ActionType.COMMAND
|
|
634
|
+
):
|
|
635
|
+
continue
|
|
636
|
+
|
|
614
637
|
# Create queue entry if new
|
|
615
638
|
if not existing:
|
|
616
639
|
queue.create_entry(
|
|
@@ -644,10 +667,10 @@ def rules_check_hook(hook_input: HookInput) -> HookOutput:
|
|
|
644
667
|
),
|
|
645
668
|
)
|
|
646
669
|
else:
|
|
647
|
-
# Command failed
|
|
648
|
-
error_msg = format_command_errors(cmd_results)
|
|
649
|
-
skip_hint = f"
|
|
650
|
-
command_errors.append(f"
|
|
670
|
+
# Command failed - format detailed error message
|
|
671
|
+
error_msg = format_command_errors(cmd_results, rule_name=rule.name)
|
|
672
|
+
skip_hint = f"\nTo skip, include `<promise>✓ {rule.name}</promise>` in your response."
|
|
673
|
+
command_errors.append(f"{error_msg}{skip_hint}")
|
|
651
674
|
queue.update_status(
|
|
652
675
|
trigger_hash,
|
|
653
676
|
QueueEntryStatus.FAILED,
|
|
@@ -662,6 +685,26 @@ def rules_check_hook(hook_input: HookInput) -> HookOutput:
|
|
|
662
685
|
# Collect for prompt output
|
|
663
686
|
prompt_results.append(result)
|
|
664
687
|
|
|
688
|
+
# Handle FAILED queue entries that have been promised
|
|
689
|
+
# (These rules weren't in results because evaluate_rules skips promised rules,
|
|
690
|
+
# but we need to update their queue status to SKIPPED)
|
|
691
|
+
if promised_rules:
|
|
692
|
+
promised_lower = {name.lower() for name in promised_rules}
|
|
693
|
+
for entry in queue.get_all_entries():
|
|
694
|
+
if (
|
|
695
|
+
entry.status == QueueEntryStatus.FAILED
|
|
696
|
+
and entry.rule_name.lower() in promised_lower
|
|
697
|
+
):
|
|
698
|
+
queue.update_status(
|
|
699
|
+
entry.trigger_hash,
|
|
700
|
+
QueueEntryStatus.SKIPPED,
|
|
701
|
+
ActionResult(
|
|
702
|
+
type="command",
|
|
703
|
+
output="Acknowledged via promise tag",
|
|
704
|
+
exit_code=None,
|
|
705
|
+
),
|
|
706
|
+
)
|
|
707
|
+
|
|
665
708
|
# Build response
|
|
666
709
|
messages: list[str] = []
|
|
667
710
|
|
|
@@ -684,17 +727,33 @@ def rules_check_hook(hook_input: HookInput) -> HookOutput:
|
|
|
684
727
|
|
|
685
728
|
def main() -> None:
|
|
686
729
|
"""Entry point for the rules check hook."""
|
|
687
|
-
# Determine platform from environment
|
|
688
730
|
platform_str = os.environ.get("DEEPWORK_HOOK_PLATFORM", "claude")
|
|
689
731
|
try:
|
|
690
732
|
platform = Platform(platform_str)
|
|
691
733
|
except ValueError:
|
|
692
734
|
platform = Platform.CLAUDE
|
|
693
735
|
|
|
694
|
-
# Run the hook with the wrapper
|
|
695
736
|
exit_code = run_hook(rules_check_hook, platform)
|
|
696
737
|
sys.exit(exit_code)
|
|
697
738
|
|
|
698
739
|
|
|
699
740
|
if __name__ == "__main__":
|
|
700
|
-
|
|
741
|
+
# Wrap entry point to catch early failures (e.g., import errors in wrapper.py)
|
|
742
|
+
try:
|
|
743
|
+
main()
|
|
744
|
+
except Exception as e:
|
|
745
|
+
# Last resort error handling - output JSON manually since wrapper may be broken
|
|
746
|
+
import json
|
|
747
|
+
import traceback
|
|
748
|
+
|
|
749
|
+
error_output = {
|
|
750
|
+
"decision": "block",
|
|
751
|
+
"reason": (
|
|
752
|
+
"## Hook Script Error\n\n"
|
|
753
|
+
f"Error type: {type(e).__name__}\n"
|
|
754
|
+
f"Error: {e}\n\n"
|
|
755
|
+
f"Traceback:\n```\n{traceback.format_exc()}\n```"
|
|
756
|
+
),
|
|
757
|
+
}
|
|
758
|
+
print(json.dumps(error_output))
|
|
759
|
+
sys.exit(0)
|
deepwork/hooks/wrapper.py
CHANGED
|
@@ -321,6 +321,55 @@ def write_stdout(data: str) -> None:
|
|
|
321
321
|
print(data)
|
|
322
322
|
|
|
323
323
|
|
|
324
|
+
def format_hook_error(
|
|
325
|
+
error: Exception,
|
|
326
|
+
context: str = "",
|
|
327
|
+
) -> dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Format an error into a blocking JSON response with detailed information.
|
|
330
|
+
|
|
331
|
+
This is used when the hook script itself fails, to provide useful
|
|
332
|
+
error information to the user instead of a generic "non-blocking status code" message.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
error: The exception that occurred
|
|
336
|
+
context: Additional context about where the error occurred
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Dict with decision="block" and detailed error message
|
|
340
|
+
"""
|
|
341
|
+
import traceback
|
|
342
|
+
|
|
343
|
+
error_type = type(error).__name__
|
|
344
|
+
error_msg = str(error)
|
|
345
|
+
tb = traceback.format_exc()
|
|
346
|
+
|
|
347
|
+
parts = ["## Hook Script Error", ""]
|
|
348
|
+
if context:
|
|
349
|
+
parts.append(f"Context: {context}")
|
|
350
|
+
parts.append(f"Error type: {error_type}")
|
|
351
|
+
parts.append(f"Error: {error_msg}")
|
|
352
|
+
parts.append("")
|
|
353
|
+
parts.append("Traceback:")
|
|
354
|
+
parts.append(f"```\n{tb}\n```")
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
"decision": "block",
|
|
358
|
+
"reason": "\n".join(parts),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def output_hook_error(error: Exception, context: str = "") -> None:
|
|
363
|
+
"""
|
|
364
|
+
Output a hook error as JSON to stdout.
|
|
365
|
+
|
|
366
|
+
Use this in exception handlers to ensure the hook always outputs
|
|
367
|
+
valid JSON even when crashing.
|
|
368
|
+
"""
|
|
369
|
+
error_dict = format_hook_error(error, context)
|
|
370
|
+
print(json.dumps(error_dict))
|
|
371
|
+
|
|
372
|
+
|
|
324
373
|
def run_hook(
|
|
325
374
|
hook_fn: Callable[[HookInput], HookOutput],
|
|
326
375
|
platform: Platform,
|
|
@@ -340,24 +389,25 @@ def run_hook(
|
|
|
340
389
|
platform: The platform calling this hook
|
|
341
390
|
|
|
342
391
|
Returns:
|
|
343
|
-
Exit code (0 for success
|
|
392
|
+
Exit code (0 for success)
|
|
344
393
|
"""
|
|
345
|
-
# Read and normalize input
|
|
346
|
-
raw_input = read_stdin()
|
|
347
|
-
hook_input = normalize_input(raw_input, platform)
|
|
348
|
-
|
|
349
|
-
# Call the hook
|
|
350
394
|
try:
|
|
395
|
+
# Read and normalize input
|
|
396
|
+
raw_input = read_stdin()
|
|
397
|
+
hook_input = normalize_input(raw_input, platform)
|
|
398
|
+
|
|
399
|
+
# Call the hook
|
|
351
400
|
hook_output = hook_fn(hook_input)
|
|
352
|
-
except Exception as e:
|
|
353
|
-
# On error, allow the action but log
|
|
354
|
-
print(f"Hook error: {e}", file=sys.stderr)
|
|
355
|
-
hook_output = HookOutput()
|
|
356
401
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
402
|
+
# Denormalize and write output
|
|
403
|
+
output_json = denormalize_output(hook_output, platform, hook_input.event)
|
|
404
|
+
write_stdout(output_json)
|
|
360
405
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
406
|
+
# Always return 0 when using JSON output format
|
|
407
|
+
# The decision field in the JSON controls blocking behavior
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
# On any error, output a proper JSON error response
|
|
412
|
+
output_hook_error(e, context=f"Running hook {hook_fn.__name__}")
|
|
413
|
+
return 0 # Return 0 so Claude Code processes our JSON output
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""JSON Schema definition for doc specs (document type definitions)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
# Schema for a single quality criterion
|
|
6
|
+
QUALITY_CRITERION_SCHEMA: dict[str, Any] = {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["name", "description"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "Short name for the quality criterion",
|
|
14
|
+
},
|
|
15
|
+
"description": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Detailed description of what this criterion requires",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": False,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Schema for doc spec frontmatter
|
|
25
|
+
DOC_SPEC_FRONTMATTER_SCHEMA: dict[str, Any] = {
|
|
26
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": ["name", "description", "quality_criteria"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"minLength": 1,
|
|
33
|
+
"description": "Human-readable name for the document type",
|
|
34
|
+
},
|
|
35
|
+
"description": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"minLength": 1,
|
|
38
|
+
"description": "Description of this document type's purpose",
|
|
39
|
+
},
|
|
40
|
+
"path_patterns": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"description": "Glob patterns for where documents of this type should be stored",
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"target_audience": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Who this document is written for",
|
|
51
|
+
},
|
|
52
|
+
"frequency": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "How often this document type is produced (e.g., 'Monthly', 'Per sprint')",
|
|
55
|
+
},
|
|
56
|
+
"quality_criteria": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"description": "Quality criteria that documents of this type must meet",
|
|
59
|
+
"minItems": 1,
|
|
60
|
+
"items": QUALITY_CRITERION_SCHEMA,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": False,
|
|
64
|
+
}
|
deepwork/schemas/job_schema.py
CHANGED
|
@@ -161,10 +161,32 @@ JOB_SCHEMA: dict[str, Any] = {
|
|
|
161
161
|
},
|
|
162
162
|
"outputs": {
|
|
163
163
|
"type": "array",
|
|
164
|
-
"description": "List of output files/directories",
|
|
164
|
+
"description": "List of output files/directories, optionally with document type references",
|
|
165
165
|
"items": {
|
|
166
|
-
"
|
|
167
|
-
|
|
166
|
+
"oneOf": [
|
|
167
|
+
{
|
|
168
|
+
"type": "string",
|
|
169
|
+
"minLength": 1,
|
|
170
|
+
"description": "Simple output file path (backward compatible)",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "object",
|
|
174
|
+
"required": ["file"],
|
|
175
|
+
"properties": {
|
|
176
|
+
"file": {
|
|
177
|
+
"type": "string",
|
|
178
|
+
"minLength": 1,
|
|
179
|
+
"description": "Output file path",
|
|
180
|
+
},
|
|
181
|
+
"doc_spec": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"pattern": r"^\.deepwork/doc_specs/[a-z][a-z0-9_-]*\.md$",
|
|
184
|
+
"description": "Path to doc spec file",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
"additionalProperties": False,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
168
190
|
},
|
|
169
191
|
},
|
|
170
192
|
"dependencies": {
|