moai-adk 0.10.1__py3-none-any.whl → 0.11.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.

Potentially problematic release.


This version of moai-adk might be problematic. Click here for more details.

Files changed (58) hide show
  1. moai_adk/core/issue_creator.py +2 -2
  2. moai_adk/core/project/detector.py +285 -12
  3. moai_adk/core/project/phase_executor.py +4 -0
  4. moai_adk/core/tags/ci_validator.py +33 -3
  5. moai_adk/core/template_engine.py +6 -2
  6. moai_adk/templates/.claude/commands/alfred/0-project.md +60 -62
  7. moai_adk/templates/.claude/commands/alfred/1-plan.md +6 -0
  8. moai_adk/templates/.claude/commands/alfred/2-run.md +6 -0
  9. moai_adk/templates/.claude/commands/alfred/3-sync.md +6 -0
  10. moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +8 -9
  11. moai_adk/templates/.claude/hooks/alfred/core/project.py +22 -28
  12. moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
  13. moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +109 -0
  14. moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +4 -4
  15. moai_adk/templates/.claude/hooks/alfred/notification__handle_events.py +10 -15
  16. moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +10 -15
  17. moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +10 -15
  18. moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +10 -15
  19. moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +10 -15
  20. moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +2 -2
  21. moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +19 -26
  22. moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +55 -23
  23. moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +4 -4
  24. moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +134 -3
  25. moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +9 -10
  26. moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +3 -6
  27. moai_adk/templates/.claude/hooks/alfred/stop__handle_interrupt.py +10 -15
  28. moai_adk/templates/.claude/hooks/alfred/subagent_stop__handle_subagent_end.py +10 -15
  29. moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +11 -20
  30. moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
  31. moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +136 -0
  32. moai_adk/templates/.github/workflows/c-tag-validation.yml +83 -0
  33. moai_adk/templates/.github/workflows/cpp-tag-validation.yml +79 -0
  34. moai_adk/templates/.github/workflows/csharp-tag-validation.yml +65 -0
  35. moai_adk/templates/.github/workflows/dart-tag-validation.yml +82 -0
  36. moai_adk/templates/.github/workflows/java-tag-validation.yml +75 -0
  37. moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +67 -0
  38. moai_adk/templates/.github/workflows/{release.yml → moai-adk-release.yml} +6 -2
  39. moai_adk/templates/.github/workflows/{tag-validation.yml → moai-adk-tag-validation.yml} +53 -8
  40. moai_adk/templates/.github/workflows/moai-gitflow.yml +6 -1
  41. moai_adk/templates/.github/workflows/php-tag-validation.yml +56 -0
  42. moai_adk/templates/.github/workflows/ruby-tag-validation.yml +68 -0
  43. moai_adk/templates/.github/workflows/rust-tag-validation.yml +73 -0
  44. moai_adk/templates/.github/workflows/shell-tag-validation.yml +65 -0
  45. moai_adk/templates/.github/workflows/swift-tag-validation.yml +79 -0
  46. moai_adk/templates/.moai/memory/GITFLOW-PROTECTION-POLICY.md +330 -0
  47. moai_adk/templates/.moai/memory/SPEC-METADATA.md +356 -0
  48. moai_adk/templates/CLAUDE.md +536 -65
  49. moai_adk/templates/workflows/go-tag-validation.yml +130 -0
  50. moai_adk/templates/workflows/javascript-tag-validation.yml +135 -0
  51. moai_adk/templates/workflows/python-tag-validation.yml +118 -0
  52. moai_adk/templates/workflows/typescript-tag-validation.yml +154 -0
  53. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/METADATA +70 -13
  54. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/RECORD +58 -37
  55. /moai_adk/templates/.github/workflows/{spec-issue-sync.yml → moai-adk-spec-issue-sync.yml} +0 -0
  56. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/WHEEL +0 -0
  57. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/entry_points.txt +0 -0
  58. {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -4,21 +4,152 @@
4
4
  Notification, Stop, SubagentStop event handling
5
5
  """
6
6
 
7
+ import json
8
+ from datetime import datetime, timedelta
9
+
10
+ from utils.timeout import CrossPlatformTimeout, TimeoutError as PlatformTimeoutError
11
+ from pathlib import Path
12
+
7
13
  from core import HookPayload, HookResult
8
14
 
9
15
 
16
+ def _get_command_state_file(cwd: str) -> Path:
17
+ """Get the path to command state tracking file"""
18
+ state_dir = Path(cwd) / ".moai" / "memory"
19
+ state_dir.mkdir(parents=True, exist_ok=True)
20
+ return state_dir / "command-execution-state.json"
21
+
22
+
23
+ def _load_command_state(cwd: str) -> dict:
24
+ """Load current command execution state"""
25
+ try:
26
+ state_file = _get_command_state_file(cwd)
27
+ if state_file.exists():
28
+ with open(state_file, "r", encoding="utf-8") as f:
29
+ return json.load(f)
30
+ except Exception:
31
+ pass
32
+ return {"last_command": None, "last_timestamp": None, "is_running": False}
33
+
34
+
35
+ def _save_command_state(cwd: str, state: dict) -> None:
36
+ """Save command execution state"""
37
+ try:
38
+ state_file = _get_command_state_file(cwd)
39
+ with open(state_file, "w", encoding="utf-8") as f:
40
+ json.dump(state, f, indent=2)
41
+ except Exception:
42
+ pass
43
+
44
+
45
+ def _is_duplicate_command(current_cmd: str, last_cmd: str, last_timestamp: str) -> bool:
46
+ """Check if current command is a duplicate of the last one within 3 seconds"""
47
+ if not last_cmd or not last_timestamp or current_cmd != last_cmd:
48
+ return False
49
+
50
+ try:
51
+ last_time = datetime.fromisoformat(last_timestamp)
52
+ current_time = datetime.now()
53
+ time_diff = (current_time - last_time).total_seconds()
54
+ # Consider it a duplicate if same command within 3 seconds
55
+ return time_diff < 3
56
+ except Exception:
57
+ return False
58
+
59
+
10
60
  def handle_notification(payload: HookPayload) -> HookResult:
11
- """Notification event handler (default implementation)"""
61
+ """Notification event handler
62
+
63
+ Detects and warns about duplicate command executions
64
+ (When the same /alfred: command is triggered multiple times within 3 seconds)
65
+ """
66
+ cwd = payload.get("cwd", ".")
67
+ notification = payload.get("notification", {})
68
+
69
+ # Extract command information from notification
70
+ current_cmd = None
71
+ if isinstance(notification, dict):
72
+ # Check if notification contains command information
73
+ text = notification.get("text", "") or str(notification)
74
+ if "/alfred:" in text:
75
+ # Extract /alfred: command
76
+ import re
77
+
78
+ match = re.search(r"/alfred:\S+", text)
79
+ if match:
80
+ current_cmd = match.group()
81
+
82
+ if not current_cmd:
83
+ return HookResult()
84
+
85
+ # Load current state
86
+ state = _load_command_state(cwd)
87
+ last_cmd = state.get("last_command")
88
+ last_timestamp = state.get("last_timestamp")
89
+
90
+ # Check for duplicate
91
+ if _is_duplicate_command(current_cmd, last_cmd, last_timestamp):
92
+ warning_msg = (
93
+ f"⚠️ Duplicate command detected: '{current_cmd}' "
94
+ f"is running multiple times within 3 seconds.\n"
95
+ f"This may indicate a system issue. Check logs in `.moai/logs/command-invocations.log`"
96
+ )
97
+
98
+ # Update state - mark as duplicate detected
99
+ state["duplicate_detected"] = True
100
+ state["duplicate_command"] = current_cmd
101
+ state["duplicate_timestamp"] = datetime.now().isoformat()
102
+ _save_command_state(cwd, state)
103
+
104
+ return HookResult(system_message=warning_msg, continue_execution=True)
105
+
106
+ # Update state with current command
107
+ state["last_command"] = current_cmd
108
+ state["last_timestamp"] = datetime.now().isoformat()
109
+ state["is_running"] = True
110
+ state["duplicate_detected"] = False
111
+ _save_command_state(cwd, state)
112
+
12
113
  return HookResult()
13
114
 
14
115
 
15
116
  def handle_stop(payload: HookPayload) -> HookResult:
16
- """Stop event handler (default implementation)"""
117
+ """Stop event handler
118
+
119
+ Marks command execution as complete
120
+ """
121
+ cwd = payload.get("cwd", ".")
122
+ state = _load_command_state(cwd)
123
+ state["is_running"] = False
124
+ state["last_timestamp"] = datetime.now().isoformat()
125
+ _save_command_state(cwd, state)
126
+
17
127
  return HookResult()
18
128
 
19
129
 
20
130
  def handle_subagent_stop(payload: HookPayload) -> HookResult:
21
- """SubagentStop event handler (default implementation)"""
131
+ """SubagentStop event handler
132
+
133
+ Records when a sub-agent finishes execution
134
+ """
135
+ cwd = payload.get("cwd", ".")
136
+
137
+ # Extract subagent name if available
138
+ subagent_name = (
139
+ payload.get("subagent", {}).get("name")
140
+ if isinstance(payload.get("subagent"), dict)
141
+ else None
142
+ )
143
+
144
+ try:
145
+ state_file = _get_command_state_file(cwd).parent / "subagent-execution.log"
146
+ timestamp = datetime.now().isoformat()
147
+
148
+ with open(state_file, "a", encoding="utf-8") as f:
149
+ f.write(f"{timestamp} | Subagent Stop | {subagent_name}\n")
150
+ except Exception:
151
+ pass
152
+
22
153
  return HookResult()
23
154
 
24
155
 
@@ -6,7 +6,7 @@ SessionStart, SessionEnd event handling
6
6
 
7
7
  from core import HookPayload, HookResult
8
8
  from core.checkpoint import list_checkpoints
9
- from core.project import count_specs, detect_language, get_git_info, get_package_version_info
9
+ from core.project import count_specs, get_git_info, get_package_version_info
10
10
 
11
11
 
12
12
  def handle_session_start(payload: HookPayload) -> HookResult:
@@ -25,14 +25,14 @@ def handle_session_start(payload: HookPayload) -> HookResult:
25
25
 
26
26
  Message Format:
27
27
  🚀 MoAI-ADK Session Started
28
- Language: {language}
28
+ [Version: {version}] - optional if version check fails
29
29
  [Branch: {branch} ({commit hash})] - optional if git fails
30
30
  [Changes: {Number of Changed Files}] - optional if git fails
31
31
  [SPEC Progress: {Complete}/{Total} ({percent}%)] - optional if specs fail
32
32
  [Checkpoints: {number} available] - optional if checkpoint list fails
33
33
 
34
34
  Graceful Degradation Strategy:
35
- - CRITICAL: Language detection (must succeed - no try-except)
35
+ - OPTIONAL: Version info (skip if timeout/failure)
36
36
  - OPTIONAL: Git info (skip if timeout/failure)
37
37
  - OPTIONAL: SPEC progress (skip if timeout/failure)
38
38
  - OPTIONAL: Checkpoint list (skip if timeout/failure)
@@ -66,9 +66,6 @@ def handle_session_start(payload: HookPayload) -> HookResult:
66
66
 
67
67
  cwd = payload.get("cwd", ".")
68
68
 
69
- # CRITICAL: Language detection - MUST succeed (no try-except)
70
- language = detect_language(cwd)
71
-
72
69
  # OPTIONAL: Git info - skip if timeout/failure
73
70
  git_info = {}
74
71
  try:
@@ -119,13 +116,17 @@ def handle_session_start(payload: HookPayload) -> HookResult:
119
116
  # Check if this is a major version update
120
117
  if version_info.get("is_major_update"):
121
118
  # Major version warning
122
- lines.append(f" ⚠️ Major version update available: {version_info['current']} → {version_info['latest']}")
119
+ lines.append(
120
+ f" ⚠️ Major version update available: {version_info['current']} → {version_info['latest']}"
121
+ )
123
122
  lines.append(" Breaking changes detected. Review release notes:")
124
123
  if version_info.get("release_notes_url"):
125
124
  lines.append(f" 📝 {version_info['release_notes_url']}")
126
125
  else:
127
126
  # Regular update
128
- lines.append(f" 🗿 MoAI-ADK Ver: {version_info['current']} → {version_info['latest']} available ✨")
127
+ lines.append(
128
+ f" 🗿 MoAI-ADK Ver: {version_info['current']} → {version_info['latest']} available ✨"
129
+ )
129
130
  if version_info.get("release_notes_url"):
130
131
  lines.append(f" 📝 Release Notes: {version_info['release_notes_url']}")
131
132
 
@@ -136,8 +137,6 @@ def handle_session_start(payload: HookPayload) -> HookResult:
136
137
  # No update available - show current version only
137
138
  lines.append(f" 🗿 MoAI-ADK Ver: {version_info['current']}")
138
139
 
139
- # Add language info
140
- lines.append(f" 🐍 Language: {language}")
141
140
 
142
141
  # Add Git info only if available (not degraded)
143
142
  if git_info:
@@ -54,8 +54,7 @@ def handle_pre_tool_use(payload: HookPayload) -> HookResult:
54
54
  checkpoint_branch = create_checkpoint(cwd, operation_type)
55
55
  if checkpoint_branch != "checkpoint-failed":
56
56
  system_message = (
57
- f"🛡️ Checkpoint created: {checkpoint_branch}\n"
58
- f" Operation: {operation_type}"
57
+ f"🛡️ Checkpoint created: {checkpoint_branch}\n Operation: {operation_type}"
59
58
  )
60
59
  return HookResult(system_message=system_message, continue_execution=True)
61
60
  except Exception:
@@ -66,10 +65,8 @@ def handle_pre_tool_use(payload: HookPayload) -> HookResult:
66
65
  issues = scan_recent_changes_for_missing_tags(cwd)
67
66
  if issues:
68
67
  # Summarize first few issues for display
69
- preview = "\n".join(
70
- f" - {i.path} 기대 태그: {i.expected}" for i in issues[:5]
71
- )
72
- more = "" if len(issues) <= 5 else f"\n (외 {len(issues)-5}건 더 존재)"
68
+ preview = "\n".join(f" - {i.path} → 기대 태그: {i.expected}" for i in issues[:5])
69
+ more = "" if len(issues) <= 5 else f"\n (외 {len(issues) - 5}건 더 존재)"
73
70
  msg = (
74
71
  "⚠️ TAG 누락 감지: 생성/수정한 파일 중 @TAG가 없는 항목이 있습니다.\n"
75
72
  f"{preview}{more}\n"
@@ -10,9 +10,10 @@ Output: Continue execution (currently a stub for future enhancements)
10
10
  """
11
11
 
12
12
  import json
13
- import signal
14
13
  import sys
15
- from pathlib import Path
14
+ from pathlib import
15
+ from utils.timeout import CrossPlatformTimeout, TimeoutError as PlatformTimeoutError
16
+ Path
16
17
  from typing import Any
17
18
 
18
19
  # Setup import path for shared modules
@@ -24,15 +25,9 @@ if str(SHARED_DIR) not in sys.path:
24
25
  from handlers import handle_stop
25
26
 
26
27
 
27
- class HookTimeoutError(Exception):
28
- """Hook execution timeout exception"""
29
28
  pass
30
29
 
31
30
 
32
- def _timeout_handler(signum, frame):
33
- """Signal handler for 5-second timeout"""
34
- raise HookTimeoutError("Hook execution exceeded 5-second timeout")
35
-
36
31
 
37
32
  def main() -> None:
38
33
  """Main entry point for Stop hook
@@ -48,8 +43,8 @@ def main() -> None:
48
43
  1: Error (timeout, JSON parse failure, handler exception)
49
44
  """
50
45
  # Set 5-second timeout
51
- signal.signal(signal.SIGALRM, _timeout_handler)
52
- signal.alarm(5)
46
+ timeout = CrossPlatformTimeout(5)
47
+ timeout.start()
53
48
 
54
49
  try:
55
50
  # Read JSON payload from stdin
@@ -63,11 +58,11 @@ def main() -> None:
63
58
  print(json.dumps(result.to_dict()))
64
59
  sys.exit(0)
65
60
 
66
- except HookTimeoutError:
61
+ except PlatformTimeoutError:
67
62
  # Timeout - return minimal valid response
68
63
  timeout_response: dict[str, Any] = {
69
64
  "continue": True,
70
- "systemMessage": "⚠️ Stop handler timeout"
65
+ "systemMessage": "⚠️ Stop handler timeout",
71
66
  }
72
67
  print(json.dumps(timeout_response))
73
68
  print("Stop hook timeout after 5 seconds", file=sys.stderr)
@@ -77,7 +72,7 @@ def main() -> None:
77
72
  # JSON parse error
78
73
  error_response: dict[str, Any] = {
79
74
  "continue": True,
80
- "hookSpecificOutput": {"error": f"JSON parse error: {e}"}
75
+ "hookSpecificOutput": {"error": f"JSON parse error: {e}"},
81
76
  }
82
77
  print(json.dumps(error_response))
83
78
  print(f"Stop JSON parse error: {e}", file=sys.stderr)
@@ -87,7 +82,7 @@ def main() -> None:
87
82
  # Unexpected error
88
83
  error_response: dict[str, Any] = {
89
84
  "continue": True,
90
- "hookSpecificOutput": {"error": f"Stop error: {e}"}
85
+ "hookSpecificOutput": {"error": f"Stop error: {e}"},
91
86
  }
92
87
  print(json.dumps(error_response))
93
88
  print(f"Stop unexpected error: {e}", file=sys.stderr)
@@ -95,7 +90,7 @@ def main() -> None:
95
90
 
96
91
  finally:
97
92
  # Always cancel alarm
98
- signal.alarm(0)
93
+ timeout.cancel()
99
94
 
100
95
 
101
96
  if __name__ == "__main__":
@@ -10,9 +10,10 @@ Output: Continue execution (currently a stub for future enhancements)
10
10
  """
11
11
 
12
12
  import json
13
- import signal
14
13
  import sys
15
- from pathlib import Path
14
+ from pathlib import
15
+ from utils.timeout import CrossPlatformTimeout, TimeoutError as PlatformTimeoutError
16
+ Path
16
17
  from typing import Any
17
18
 
18
19
  # Setup import path for shared modules
@@ -24,15 +25,9 @@ if str(SHARED_DIR) not in sys.path:
24
25
  from handlers import handle_subagent_stop
25
26
 
26
27
 
27
- class HookTimeoutError(Exception):
28
- """Hook execution timeout exception"""
29
28
  pass
30
29
 
31
30
 
32
- def _timeout_handler(signum, frame):
33
- """Signal handler for 5-second timeout"""
34
- raise HookTimeoutError("Hook execution exceeded 5-second timeout")
35
-
36
31
 
37
32
  def main() -> None:
38
33
  """Main entry point for SubagentStop hook
@@ -48,8 +43,8 @@ def main() -> None:
48
43
  1: Error (timeout, JSON parse failure, handler exception)
49
44
  """
50
45
  # Set 5-second timeout
51
- signal.signal(signal.SIGALRM, _timeout_handler)
52
- signal.alarm(5)
46
+ timeout = CrossPlatformTimeout(5)
47
+ timeout.start()
53
48
 
54
49
  try:
55
50
  # Read JSON payload from stdin
@@ -63,11 +58,11 @@ def main() -> None:
63
58
  print(json.dumps(result.to_dict()))
64
59
  sys.exit(0)
65
60
 
66
- except HookTimeoutError:
61
+ except PlatformTimeoutError:
67
62
  # Timeout - return minimal valid response
68
63
  timeout_response: dict[str, Any] = {
69
64
  "continue": True,
70
- "systemMessage": "⚠️ SubagentStop handler timeout"
65
+ "systemMessage": "⚠️ SubagentStop handler timeout",
71
66
  }
72
67
  print(json.dumps(timeout_response))
73
68
  print("SubagentStop hook timeout after 5 seconds", file=sys.stderr)
@@ -77,7 +72,7 @@ def main() -> None:
77
72
  # JSON parse error
78
73
  error_response: dict[str, Any] = {
79
74
  "continue": True,
80
- "hookSpecificOutput": {"error": f"JSON parse error: {e}"}
75
+ "hookSpecificOutput": {"error": f"JSON parse error: {e}"},
81
76
  }
82
77
  print(json.dumps(error_response))
83
78
  print(f"SubagentStop JSON parse error: {e}", file=sys.stderr)
@@ -87,7 +82,7 @@ def main() -> None:
87
82
  # Unexpected error
88
83
  error_response: dict[str, Any] = {
89
84
  "continue": True,
90
- "hookSpecificOutput": {"error": f"SubagentStop error: {e}"}
85
+ "hookSpecificOutput": {"error": f"SubagentStop error: {e}"},
91
86
  }
92
87
  print(json.dumps(error_response))
93
88
  print(f"SubagentStop unexpected error: {e}", file=sys.stderr)
@@ -95,7 +90,7 @@ def main() -> None:
95
90
 
96
91
  finally:
97
92
  # Always cancel alarm
98
- signal.alarm(0)
93
+ timeout.cancel()
99
94
 
100
95
 
101
96
  if __name__ == "__main__":
@@ -10,9 +10,9 @@ Output: additionalContext with document path suggestions
10
10
  """
11
11
 
12
12
  import json
13
- import signal
14
13
  import sys
15
14
  from pathlib import Path
15
+ from utils.timeout import CrossPlatformTimeout, TimeoutError as PlatformTimeoutError
16
16
  from typing import Any
17
17
 
18
18
  # Setup import path for shared modules
@@ -24,15 +24,6 @@ if str(SHARED_DIR) not in sys.path:
24
24
  from handlers import handle_user_prompt_submit
25
25
 
26
26
 
27
- class HookTimeoutError(Exception):
28
- """Hook execution timeout exception"""
29
- pass
30
-
31
-
32
- def _timeout_handler(signum, frame):
33
- """Signal handler for 5-second timeout"""
34
- raise HookTimeoutError("Hook execution exceeded 5-second timeout")
35
-
36
27
 
37
28
  def main() -> None:
38
29
  """Main entry point for UserPromptSubmit hook
@@ -57,8 +48,8 @@ def main() -> None:
57
48
  }
58
49
  """
59
50
  # Set 5-second timeout
60
- signal.signal(signal.SIGALRM, _timeout_handler)
61
- signal.alarm(5)
51
+ timeout = CrossPlatformTimeout(5)
52
+ timeout.start()
62
53
 
63
54
  try:
64
55
  # Read JSON payload from stdin
@@ -72,14 +63,14 @@ def main() -> None:
72
63
  print(json.dumps(result.to_user_prompt_submit_dict()))
73
64
  sys.exit(0)
74
65
 
75
- except HookTimeoutError:
66
+ except PlatformTimeoutError:
76
67
  # Timeout - return minimal valid response
77
68
  timeout_response: dict[str, Any] = {
78
69
  "continue": True,
79
70
  "hookSpecificOutput": {
80
71
  "hookEventName": "UserPromptSubmit",
81
- "additionalContext": "⚠️ JIT context timeout - continuing without suggestions"
82
- }
72
+ "additionalContext": "⚠️ JIT context timeout - continuing without suggestions",
73
+ },
83
74
  }
84
75
  print(json.dumps(timeout_response))
85
76
  print("UserPromptSubmit hook timeout after 5 seconds", file=sys.stderr)
@@ -91,8 +82,8 @@ def main() -> None:
91
82
  "continue": True,
92
83
  "hookSpecificOutput": {
93
84
  "hookEventName": "UserPromptSubmit",
94
- "error": f"JSON parse error: {e}"
95
- }
85
+ "error": f"JSON parse error: {e}",
86
+ },
96
87
  }
97
88
  print(json.dumps(error_response))
98
89
  print(f"UserPromptSubmit JSON parse error: {e}", file=sys.stderr)
@@ -104,8 +95,8 @@ def main() -> None:
104
95
  "continue": True,
105
96
  "hookSpecificOutput": {
106
97
  "hookEventName": "UserPromptSubmit",
107
- "error": f"UserPromptSubmit error: {e}"
108
- }
98
+ "error": f"UserPromptSubmit error: {e}",
99
+ },
109
100
  }
110
101
  print(json.dumps(error_response))
111
102
  print(f"UserPromptSubmit unexpected error: {e}", file=sys.stderr)
@@ -113,7 +104,7 @@ def main() -> None:
113
104
 
114
105
  finally:
115
106
  # Always cancel alarm
116
- signal.alarm(0)
107
+ timeout.cancel()
117
108
 
118
109
 
119
110
  if __name__ == "__main__":
@@ -0,0 +1 @@
1
+ """Utility modules for Alfred hooks."""
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ # @CODE:BUGFIX-001:CROSS-PLATFORM-TIMEOUT | SPEC: SPEC-BUGFIX-001
3
+ """Cross-Platform Timeout Handler for Windows & Unix Compatibility
4
+
5
+ Provides a unified timeout mechanism that works on both Windows (threading-based)
6
+ and Unix/POSIX systems (signal-based).
7
+
8
+ Architecture:
9
+ - Windows: threading.Timer with exception injection
10
+ - Unix/POSIX: signal.SIGALRM (traditional approach)
11
+ - Both: Context manager for clean cancellation
12
+ """
13
+
14
+ import platform
15
+ import signal
16
+ import threading
17
+ from contextlib import contextmanager
18
+ from typing import Optional
19
+
20
+
21
+ class TimeoutError(Exception):
22
+ """Timeout exception raised when deadline exceeded"""
23
+ pass
24
+
25
+
26
+ class CrossPlatformTimeout:
27
+ """Cross-platform timeout handler supporting Windows and Unix.
28
+
29
+ Windows: Uses threading.Timer to schedule timeout exception
30
+ Unix: Uses signal.SIGALRM for timeout handling
31
+
32
+ Usage:
33
+ # Context manager (recommended)
34
+ with CrossPlatformTimeout(5):
35
+ long_running_operation()
36
+
37
+ # Manual control
38
+ timeout = CrossPlatformTimeout(5)
39
+ timeout.start()
40
+ try:
41
+ long_running_operation()
42
+ finally:
43
+ timeout.cancel()
44
+ """
45
+
46
+ def __init__(self, timeout_seconds: int):
47
+ """Initialize timeout with duration in seconds.
48
+
49
+ Args:
50
+ timeout_seconds: Timeout duration in seconds
51
+ """
52
+ self.timeout_seconds = timeout_seconds
53
+ self.timer: Optional[threading.Timer] = None
54
+ self._is_windows = platform.system() == "Windows"
55
+ self._old_handler = None
56
+
57
+ def start(self) -> None:
58
+ """Start timeout countdown."""
59
+ if self._is_windows:
60
+ self._start_windows_timeout()
61
+ else:
62
+ self._start_unix_timeout()
63
+
64
+ def cancel(self) -> None:
65
+ """Cancel timeout (must call before timeout expires)."""
66
+ if self._is_windows:
67
+ self._cancel_windows_timeout()
68
+ else:
69
+ self._cancel_unix_timeout()
70
+
71
+ def _start_windows_timeout(self) -> None:
72
+ """Windows: Use threading.Timer to raise exception."""
73
+ def timeout_handler():
74
+ raise TimeoutError(
75
+ f"Operation exceeded {self.timeout_seconds}s timeout (Windows threading)"
76
+ )
77
+
78
+ self.timer = threading.Timer(self.timeout_seconds, timeout_handler)
79
+ self.timer.daemon = True
80
+ self.timer.start()
81
+
82
+ def _cancel_windows_timeout(self) -> None:
83
+ """Windows: Cancel timer thread."""
84
+ if self.timer:
85
+ self.timer.cancel()
86
+ self.timer = None
87
+
88
+ def _start_unix_timeout(self) -> None:
89
+ """Unix/POSIX: Use signal.SIGALRM for timeout."""
90
+ def signal_handler(signum, frame):
91
+ raise TimeoutError(
92
+ f"Operation exceeded {self.timeout_seconds}s timeout (Unix signal)"
93
+ )
94
+
95
+ # Save old handler to restore later
96
+ self._old_handler = signal.signal(signal.SIGALRM, signal_handler)
97
+ signal.alarm(self.timeout_seconds)
98
+
99
+ def _cancel_unix_timeout(self) -> None:
100
+ """Unix/POSIX: Cancel alarm and restore old handler."""
101
+ signal.alarm(0) # Cancel pending alarm
102
+ if self._old_handler is not None:
103
+ signal.signal(signal.SIGALRM, self._old_handler)
104
+ self._old_handler = None
105
+
106
+ def __enter__(self):
107
+ """Context manager entry."""
108
+ self.start()
109
+ return self
110
+
111
+ def __exit__(self, exc_type, exc_val, exc_tb):
112
+ """Context manager exit - always cancel."""
113
+ self.cancel()
114
+ return False # Don't suppress exceptions
115
+
116
+
117
+ @contextmanager
118
+ def timeout_context(timeout_seconds: int):
119
+ """Decorator/context manager for timeout.
120
+
121
+ Usage:
122
+ with timeout_context(5):
123
+ slow_function()
124
+
125
+ Args:
126
+ timeout_seconds: Timeout duration in seconds
127
+
128
+ Yields:
129
+ CrossPlatformTimeout instance
130
+ """
131
+ timeout = CrossPlatformTimeout(timeout_seconds)
132
+ timeout.start()
133
+ try:
134
+ yield timeout
135
+ finally:
136
+ timeout.cancel()