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.
Files changed (41) hide show
  1. deepwork/cli/hook.py +70 -0
  2. deepwork/cli/install.py +58 -14
  3. deepwork/cli/main.py +4 -0
  4. deepwork/cli/rules.py +32 -0
  5. deepwork/cli/sync.py +27 -1
  6. deepwork/core/adapters.py +213 -0
  7. deepwork/core/command_executor.py +26 -9
  8. deepwork/core/doc_spec_parser.py +205 -0
  9. deepwork/core/generator.py +79 -4
  10. deepwork/core/hooks_syncer.py +15 -2
  11. deepwork/core/parser.py +64 -2
  12. deepwork/hooks/__init__.py +9 -3
  13. deepwork/hooks/check_version.sh +230 -0
  14. deepwork/hooks/claude_hook.sh +13 -17
  15. deepwork/hooks/gemini_hook.sh +13 -17
  16. deepwork/hooks/rules_check.py +71 -12
  17. deepwork/hooks/wrapper.py +66 -16
  18. deepwork/schemas/doc_spec_schema.py +64 -0
  19. deepwork/schemas/job_schema.py +25 -3
  20. deepwork/standard_jobs/deepwork_jobs/doc_specs/job_spec.md +190 -0
  21. deepwork/standard_jobs/deepwork_jobs/job.yml +41 -8
  22. deepwork/standard_jobs/deepwork_jobs/steps/define.md +68 -2
  23. deepwork/standard_jobs/deepwork_jobs/steps/implement.md +3 -3
  24. deepwork/standard_jobs/deepwork_jobs/steps/learn.md +74 -5
  25. deepwork/standard_jobs/deepwork_jobs/steps/review_job_spec.md +208 -0
  26. deepwork/standard_jobs/deepwork_jobs/templates/doc_spec.md.example +86 -0
  27. deepwork/standard_jobs/deepwork_jobs/templates/doc_spec.md.template +26 -0
  28. deepwork/standard_jobs/deepwork_rules/hooks/capture_prompt_work_tree.sh +8 -0
  29. deepwork/standard_jobs/deepwork_rules/job.yml +5 -3
  30. deepwork/templates/claude/AGENTS.md +38 -0
  31. deepwork/templates/claude/skill-job-meta.md.jinja +7 -0
  32. deepwork/templates/claude/skill-job-step.md.jinja +107 -70
  33. deepwork/templates/gemini/skill-job-step.toml.jinja +18 -3
  34. deepwork/utils/fs.py +36 -0
  35. deepwork/utils/yaml_utils.py +24 -0
  36. {deepwork-0.3.1.dist-info → deepwork-0.5.1.dist-info}/METADATA +39 -2
  37. deepwork-0.5.1.dist-info/RECORD +72 -0
  38. deepwork-0.3.1.dist-info/RECORD +0 -62
  39. {deepwork-0.3.1.dist-info → deepwork-0.5.1.dist-info}/WHEEL +0 -0
  40. {deepwork-0.3.1.dist-info → deepwork-0.5.1.dist-info}/entry_points.txt +0 -0
  41. {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 "$@"
@@ -6,14 +6,13 @@
6
6
  # and work on any supported platform.
7
7
  #
8
8
  # Usage:
9
- # claude_hook.sh <python_hook_module>
9
+ # claude_hook.sh <hook_name>
10
10
  #
11
11
  # Example:
12
- # claude_hook.sh deepwork.hooks.rules_check
12
+ # claude_hook.sh rules_check
13
13
  #
14
- # The Python module should implement a main() function that:
15
- # 1. Calls deepwork.hooks.wrapper.run_hook() with a hook function
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 Python module to run
30
- PYTHON_MODULE="${1:-}"
28
+ # Get the hook name to run
29
+ HOOK_NAME="${1:-}"
31
30
 
32
- if [ -z "${PYTHON_MODULE}" ]; then
33
- echo "Usage: claude_hook.sh <python_hook_module>" >&2
34
- echo "Example: claude_hook.sh deepwork.hooks.rules_check" >&2
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 Python module
43
+ # Set platform environment variable for the hook
45
44
  export DEEPWORK_HOOK_PLATFORM="claude"
46
45
 
47
- # Run the Python module, passing the input via stdin
48
- # The Python module is responsible for:
49
- # 1. Reading stdin (normalized by wrapper)
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}
@@ -6,14 +6,13 @@
6
6
  # and work on any supported platform.
7
7
  #
8
8
  # Usage:
9
- # gemini_hook.sh <python_hook_module>
9
+ # gemini_hook.sh <hook_name>
10
10
  #
11
11
  # Example:
12
- # gemini_hook.sh deepwork.hooks.rules_check
12
+ # gemini_hook.sh rules_check
13
13
  #
14
- # The Python module should implement a main() function that:
15
- # 1. Calls deepwork.hooks.wrapper.run_hook() with a hook function
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 Python module to run
30
- PYTHON_MODULE="${1:-}"
28
+ # Get the hook name to run
29
+ HOOK_NAME="${1:-}"
31
30
 
32
- if [ -z "${PYTHON_MODULE}" ]; then
33
- echo "Usage: gemini_hook.sh <python_hook_module>" >&2
34
- echo "Example: gemini_hook.sh deepwork.hooks.rules_check" >&2
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 Python module
43
+ # Set platform environment variable for the hook
45
44
  export DEEPWORK_HOOK_PLATFORM="gemini"
46
45
 
47
- # Run the Python module, passing the input via stdin
48
- # The Python module is responsible for:
49
- # 1. Reading stdin (normalized by wrapper)
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}
@@ -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 deepwork.hooks.rules_check
11
- gemini_hook.sh deepwork.hooks.rules_check
9
+ Usage (via shell wrapper - recommended):
10
+ claude_hook.sh rules_check
11
+ gemini_hook.sh rules_check
12
12
 
13
- Or directly with platform environment variable:
14
- DEEPWORK_HOOK_PLATFORM=claude python -m deepwork.hooks.rules_check
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"To skip, include `<promise>✓ {rule.name}</promise>` in your response.\n"
650
- command_errors.append(f"## {rule.name}\n{error_msg}{skip_hint}")
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
- main()
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, 2 for blocking)
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
- # Denormalize and write output
358
- output_json = denormalize_output(hook_output, platform, hook_input.event)
359
- write_stdout(output_json)
402
+ # Denormalize and write output
403
+ output_json = denormalize_output(hook_output, platform, hook_input.event)
404
+ write_stdout(output_json)
360
405
 
361
- # Always return 0 when using JSON output format
362
- # The decision field in the JSON controls blocking behavior
363
- return 0
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
+ }
@@ -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
- "type": "string",
167
- "minLength": 1,
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": {