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.
- moai_adk/core/issue_creator.py +2 -2
- moai_adk/core/project/detector.py +285 -12
- moai_adk/core/project/phase_executor.py +4 -0
- moai_adk/core/tags/ci_validator.py +33 -3
- moai_adk/core/template_engine.py +6 -2
- moai_adk/templates/.claude/commands/alfred/0-project.md +60 -62
- moai_adk/templates/.claude/commands/alfred/1-plan.md +6 -0
- moai_adk/templates/.claude/commands/alfred/2-run.md +6 -0
- moai_adk/templates/.claude/commands/alfred/3-sync.md +6 -0
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +8 -9
- moai_adk/templates/.claude/hooks/alfred/core/project.py +22 -28
- moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
- moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +109 -0
- moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +4 -4
- moai_adk/templates/.claude/hooks/alfred/notification__handle_events.py +10 -15
- moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +10 -15
- moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +10 -15
- moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +10 -15
- moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +10 -15
- moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +2 -2
- moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +19 -26
- moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +55 -23
- moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +4 -4
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +134 -3
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +9 -10
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +3 -6
- moai_adk/templates/.claude/hooks/alfred/stop__handle_interrupt.py +10 -15
- moai_adk/templates/.claude/hooks/alfred/subagent_stop__handle_subagent_end.py +10 -15
- moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +11 -20
- moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
- moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +136 -0
- moai_adk/templates/.github/workflows/c-tag-validation.yml +83 -0
- moai_adk/templates/.github/workflows/cpp-tag-validation.yml +79 -0
- moai_adk/templates/.github/workflows/csharp-tag-validation.yml +65 -0
- moai_adk/templates/.github/workflows/dart-tag-validation.yml +82 -0
- moai_adk/templates/.github/workflows/java-tag-validation.yml +75 -0
- moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +67 -0
- moai_adk/templates/.github/workflows/{release.yml → moai-adk-release.yml} +6 -2
- moai_adk/templates/.github/workflows/{tag-validation.yml → moai-adk-tag-validation.yml} +53 -8
- moai_adk/templates/.github/workflows/moai-gitflow.yml +6 -1
- moai_adk/templates/.github/workflows/php-tag-validation.yml +56 -0
- moai_adk/templates/.github/workflows/ruby-tag-validation.yml +68 -0
- moai_adk/templates/.github/workflows/rust-tag-validation.yml +73 -0
- moai_adk/templates/.github/workflows/shell-tag-validation.yml +65 -0
- moai_adk/templates/.github/workflows/swift-tag-validation.yml +79 -0
- moai_adk/templates/.moai/memory/GITFLOW-PROTECTION-POLICY.md +330 -0
- moai_adk/templates/.moai/memory/SPEC-METADATA.md +356 -0
- moai_adk/templates/CLAUDE.md +536 -65
- moai_adk/templates/workflows/go-tag-validation.yml +130 -0
- moai_adk/templates/workflows/javascript-tag-validation.yml +135 -0
- moai_adk/templates/workflows/python-tag-validation.yml +118 -0
- moai_adk/templates/workflows/typescript-tag-validation.yml +154 -0
- {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/METADATA +70 -13
- {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/RECORD +58 -37
- /moai_adk/templates/.github/workflows/{spec-issue-sync.yml → moai-adk-spec-issue-sync.yml} +0 -0
- {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/WHEEL +0 -0
- {moai_adk-0.10.1.dist-info → moai_adk-0.11.1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
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()
|