moai-adk 0.9.0__py3-none-any.whl β†’ 0.15.0__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 (186) hide show
  1. moai_adk/cli/commands/init.py +14 -2
  2. moai_adk/cli/commands/update.py +214 -56
  3. moai_adk/core/issue_creator.py +2 -2
  4. moai_adk/core/project/detector.py +201 -12
  5. moai_adk/core/project/initializer.py +62 -1
  6. moai_adk/core/project/phase_executor.py +48 -6
  7. moai_adk/core/tags/ci_validator.py +34 -4
  8. moai_adk/core/tags/pre_commit_validator.py +40 -2
  9. moai_adk/core/tags/reporter.py +2 -3
  10. moai_adk/core/tags/validator.py +1 -1
  11. moai_adk/core/template_engine.py +20 -5
  12. moai_adk/templates/.claude/agents/alfred/backend-expert.md +319 -0
  13. moai_adk/templates/.claude/agents/alfred/devops-expert.md +464 -0
  14. moai_adk/templates/.claude/agents/alfred/doc-syncer.md +1 -1
  15. moai_adk/templates/.claude/agents/alfred/frontend-expert.md +357 -0
  16. moai_adk/templates/.claude/agents/alfred/git-manager.md +2 -2
  17. moai_adk/templates/.claude/agents/alfred/implementation-planner.md +76 -3
  18. moai_adk/templates/.claude/agents/alfred/project-manager.md +49 -10
  19. moai_adk/templates/.claude/agents/alfred/quality-gate.md +3 -3
  20. moai_adk/templates/.claude/agents/alfred/spec-builder.md +108 -3
  21. moai_adk/templates/.claude/agents/alfred/tag-agent.md +74 -0
  22. moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +107 -5
  23. moai_adk/templates/.claude/agents/alfred/trust-checker.md +2 -2
  24. moai_adk/templates/.claude/agents/alfred/ui-ux-expert.md +571 -0
  25. moai_adk/templates/.claude/commands/alfred/0-project.md +465 -129
  26. moai_adk/templates/.claude/commands/alfred/1-plan.md +139 -65
  27. moai_adk/templates/.claude/commands/alfred/2-run.md +214 -50
  28. moai_adk/templates/.claude/commands/alfred/3-sync.md +372 -46
  29. moai_adk/templates/.claude/commands/alfred/9-feedback.md +1 -1
  30. moai_adk/templates/.claude/hooks/alfred/core/project.py +25 -27
  31. moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
  32. moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +108 -0
  33. moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +4 -4
  34. moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +29 -0
  35. moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +11 -19
  36. moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +11 -19
  37. moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +11 -19
  38. moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +10 -18
  39. moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +2 -2
  40. moai_adk/templates/.claude/hooks/alfred/shared/core/checkpoint.py +3 -3
  41. moai_adk/templates/.claude/hooks/alfred/shared/core/context.py +5 -5
  42. moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +40 -41
  43. moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +55 -23
  44. moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +4 -4
  45. moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +132 -3
  46. moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +9 -10
  47. moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +3 -6
  48. moai_adk/templates/.claude/hooks/alfred/shared/handlers/user.py +19 -0
  49. moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +14 -22
  50. moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
  51. moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +161 -0
  52. moai_adk/templates/.claude/settings.json +5 -5
  53. moai_adk/templates/.claude/skills/moai-alfred-agent-guide/SKILL.md +70 -0
  54. moai_adk/templates/.claude/skills/moai-alfred-agent-guide/examples.md +62 -0
  55. moai_adk/templates/{.moai/memory/CLAUDE-AGENTS-GUIDE.md β†’ .claude/skills/moai-alfred-agent-guide/reference.md} +34 -0
  56. moai_adk/templates/.claude/skills/moai-alfred-config-schema/SKILL.md +56 -0
  57. moai_adk/templates/.claude/skills/moai-alfred-config-schema/examples.md +28 -0
  58. moai_adk/templates/.claude/skills/moai-alfred-config-schema/reference.md +444 -0
  59. moai_adk/templates/.claude/skills/moai-alfred-context-budget/SKILL.md +62 -0
  60. moai_adk/templates/.claude/skills/moai-alfred-context-budget/examples.md +28 -0
  61. moai_adk/templates/.claude/skills/moai-alfred-context-budget/reference.md +405 -0
  62. moai_adk/templates/.claude/skills/moai-alfred-dev-guide/SKILL.md +51 -0
  63. moai_adk/templates/.claude/skills/moai-alfred-dev-guide/examples.md +355 -0
  64. moai_adk/templates/.claude/skills/moai-alfred-dev-guide/reference.md +239 -0
  65. moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/SKILL.md +323 -0
  66. moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/examples.md +286 -0
  67. moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/reference.md +126 -0
  68. moai_adk/templates/.claude/skills/moai-alfred-gitflow-policy/SKILL.md +74 -0
  69. moai_adk/templates/.claude/skills/moai-alfred-gitflow-policy/examples.md +4 -0
  70. moai_adk/templates/.claude/skills/moai-alfred-gitflow-policy/reference.md +269 -0
  71. moai_adk/templates/.claude/skills/moai-alfred-issue-labels/SKILL.md +19 -0
  72. moai_adk/templates/.claude/skills/moai-alfred-issue-labels/examples.md +4 -0
  73. moai_adk/templates/.claude/skills/moai-alfred-persona-roles/SKILL.md +198 -0
  74. moai_adk/templates/.claude/skills/moai-alfred-persona-roles/examples.md +431 -0
  75. moai_adk/templates/.claude/skills/moai-alfred-persona-roles/reference.md +141 -0
  76. moai_adk/templates/.claude/skills/moai-alfred-practices/SKILL.md +89 -0
  77. moai_adk/templates/.claude/skills/moai-alfred-practices/examples.md +122 -0
  78. moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/SKILL.md +508 -0
  79. moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/examples.md +481 -0
  80. moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/reference.md +100 -0
  81. moai_adk/templates/.claude/skills/moai-alfred-reporting/SKILL.md +273 -0
  82. moai_adk/templates/.claude/skills/moai-alfred-rules/SKILL.md +77 -0
  83. moai_adk/templates/.claude/skills/moai-alfred-rules/examples.md +265 -0
  84. moai_adk/templates/.claude/skills/moai-alfred-session-state/SKILL.md +19 -0
  85. moai_adk/templates/.claude/skills/moai-alfred-session-state/examples.md +4 -0
  86. moai_adk/templates/.claude/skills/moai-alfred-session-state/reference.md +84 -0
  87. moai_adk/templates/.claude/skills/{moai-spec-authoring β†’ moai-alfred-spec-authoring}/SKILL.md +5 -5
  88. moai_adk/templates/.claude/skills/moai-alfred-spec-metadata-extended/SKILL.md +115 -0
  89. moai_adk/templates/.claude/skills/moai-alfred-spec-metadata-extended/examples.md +4 -0
  90. moai_adk/templates/.claude/skills/moai-alfred-spec-metadata-extended/reference.md +348 -0
  91. moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/SKILL.md +19 -0
  92. moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/examples.md +4 -0
  93. moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/reference.md +211 -0
  94. moai_adk/templates/.claude/skills/moai-alfred-workflow/SKILL.md +288 -0
  95. moai_adk/templates/.claude/skills/moai-cc-skill-descriptions/SKILL.md +19 -0
  96. moai_adk/templates/.claude/skills/moai-cc-skill-descriptions/examples.md +4 -0
  97. moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/SKILL.md +3 -3
  98. moai_adk/templates/.claude/skills/moai-design-systems/SKILL.md +802 -0
  99. moai_adk/templates/.claude/skills/moai-design-systems/examples.md +1238 -0
  100. moai_adk/templates/.claude/skills/moai-design-systems/reference.md +673 -0
  101. moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +17 -13
  102. moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +15 -12
  103. moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +14 -12
  104. moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +14 -11
  105. moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +10 -8
  106. moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +15 -12
  107. moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +13 -11
  108. moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +16 -10
  109. moai_adk/templates/.claude/skills/moai-project-documentation.md +622 -0
  110. moai_adk/templates/.git-hooks/pre-push +143 -0
  111. moai_adk/templates/.github/workflows/c-tag-validation.yml +11 -0
  112. moai_adk/templates/.github/workflows/cpp-tag-validation.yml +11 -0
  113. moai_adk/templates/.github/workflows/csharp-tag-validation.yml +11 -0
  114. moai_adk/templates/.github/workflows/dart-tag-validation.yml +11 -0
  115. moai_adk/templates/.github/workflows/go-tag-validation.yml +130 -0
  116. moai_adk/templates/.github/workflows/java-tag-validation.yml +11 -0
  117. moai_adk/templates/.github/workflows/javascript-tag-validation.yml +135 -0
  118. moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +11 -0
  119. moai_adk/templates/.github/workflows/moai-gitflow.yml +182 -25
  120. moai_adk/templates/.github/workflows/moai-release-pipeline.yml +35 -29
  121. moai_adk/templates/.github/workflows/php-tag-validation.yml +11 -0
  122. moai_adk/templates/.github/workflows/python-tag-validation.yml +118 -0
  123. moai_adk/templates/.github/workflows/release.yml +76 -7
  124. moai_adk/templates/.github/workflows/ruby-tag-validation.yml +11 -0
  125. moai_adk/templates/.github/workflows/rust-tag-validation.yml +11 -0
  126. moai_adk/templates/.github/workflows/shell-tag-validation.yml +11 -0
  127. moai_adk/templates/.github/workflows/spec-issue-sync.yml +208 -41
  128. moai_adk/templates/.github/workflows/swift-tag-validation.yml +11 -0
  129. moai_adk/templates/.github/workflows/tag-report.yml +269 -0
  130. moai_adk/templates/.github/workflows/tag-validation.yml +186 -0
  131. moai_adk/templates/.github/workflows/typescript-tag-validation.yml +154 -0
  132. moai_adk/templates/.moai/config.json +3 -1
  133. moai_adk/templates/CLAUDE.md +940 -45
  134. moai_adk/templates/workflows/go-tag-validation.yml +30 -0
  135. moai_adk/templates/workflows/javascript-tag-validation.yml +41 -0
  136. moai_adk/templates/workflows/python-tag-validation.yml +42 -0
  137. moai_adk/templates/workflows/typescript-tag-validation.yml +31 -0
  138. moai_adk/utils/banner.py +5 -5
  139. {moai_adk-0.9.0.dist-info β†’ moai_adk-0.15.0.dist-info}/METADATA +1166 -455
  140. {moai_adk-0.9.0.dist-info β†’ moai_adk-0.15.0.dist-info}/RECORD +169 -109
  141. moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +0 -209
  142. moai_adk/templates/.claude/hooks/alfred/notification__handle_events.py +0 -102
  143. moai_adk/templates/.claude/hooks/alfred/stop__handle_interrupt.py +0 -102
  144. moai_adk/templates/.claude/hooks/alfred/subagent_stop__handle_subagent_end.py +0 -102
  145. moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +0 -640
  146. moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +0 -696
  147. moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +0 -474
  148. moai_adk/templates/.github/ISSUE_TEMPLATE/spec.yml +0 -176
  149. moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +0 -69
  150. moai_adk/templates/.moai/memory/DEVELOPMENT-GUIDE.md +0 -344
  151. moai_adk/templates/.moai/memory/SPEC-METADATA.md +0 -356
  152. moai_adk/templates/.moai/memory/gitflow-protection-policy.md +0 -330
  153. moai_adk/templates/.moai/project/product.md +0 -161
  154. moai_adk/templates/.moai/project/structure.md +0 -156
  155. moai_adk/templates/.moai/project/tech.md +0 -227
  156. moai_adk/templates/README.md +0 -256
  157. moai_adk/templates/__init__.py +0 -2
  158. /moai_adk/templates/{.moai/memory/ISSUE-LABEL-MAPPING.md β†’ .claude/skills/moai-alfred-issue-labels/reference.md} +0 -0
  159. /moai_adk/templates/{.moai/memory/CLAUDE-PRACTICES.md β†’ .claude/skills/moai-alfred-practices/reference.md} +0 -0
  160. /moai_adk/templates/{.moai/memory/CLAUDE-RULES.md β†’ .claude/skills/moai-alfred-rules/reference.md} +0 -0
  161. /moai_adk/templates/.claude/skills/{moai-spec-authoring β†’ moai-alfred-spec-authoring}/README.md +0 -0
  162. /moai_adk/templates/.claude/skills/{moai-spec-authoring β†’ moai-alfred-spec-authoring}/examples/validate-spec.sh +0 -0
  163. /moai_adk/templates/.claude/skills/{moai-spec-authoring β†’ moai-alfred-spec-authoring}/examples.md +0 -0
  164. /moai_adk/templates/.claude/skills/{moai-spec-authoring β†’ moai-alfred-spec-authoring}/reference.md +0 -0
  165. /moai_adk/templates/{.moai/memory/SKILLS-DESCRIPTION-POLICY.md β†’ .claude/skills/moai-cc-skill-descriptions/reference.md} +0 -0
  166. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/CHECKLIST.md +0 -0
  167. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/EXAMPLES.md +0 -0
  168. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/INTERACTIVE-DISCOVERY.md +0 -0
  169. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/METADATA.md +0 -0
  170. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/PARALLEL-ANALYSIS-REPORT.md +0 -0
  171. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/PYTHON-VERSION-MATRIX.md +0 -0
  172. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/SKILL-FACTORY-WORKFLOW.md +0 -0
  173. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/SKILL-UPDATE-ADVISOR.md +0 -0
  174. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/STEP-BY-STEP-GUIDE.md +0 -0
  175. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/STRUCTURE.md +0 -0
  176. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/WEB-RESEARCH.md +0 -0
  177. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/reference.md +0 -0
  178. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/scripts/generate-structure.sh +0 -0
  179. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/scripts/validate-skill.sh +0 -0
  180. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/templates/SKILL_TEMPLATE.md +0 -0
  181. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/templates/examples-template.md +0 -0
  182. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/templates/reference-template.md +0 -0
  183. /moai_adk/templates/.claude/skills/{moai-skill-factory β†’ moai-cc-skill-factory}/templates/scripts-template.sh +0 -0
  184. {moai_adk-0.9.0.dist-info β†’ moai_adk-0.15.0.dist-info}/WHEEL +0 -0
  185. {moai_adk-0.9.0.dist-info β†’ moai_adk-0.15.0.dist-info}/entry_points.txt +0 -0
  186. {moai_adk-0.9.0.dist-info β†’ moai_adk-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -22,13 +22,13 @@ def get_jit_context(prompt: str, cwd: str) -> list[str]:
22
22
  If there is no matching pattern or file, an empty list []
23
23
 
24
24
  Patterns:
25
- - "/alfred:1-plan" β†’ .moai/memory/spec-metadata.md
26
- - "/alfred:2-run" β†’ .moai/memory/development-guide.md
25
+ - "/alfred:1-plan" β†’ .claude/skills/moai-alfred-spec-metadata-extended/reference.md
26
+ - "/alfred:2-run" β†’ .claude/skills/moai-alfred-dev-guide/reference.md
27
27
  - "test" β†’ tests/ (if directory exists)
28
28
 
29
29
  Examples:
30
30
  >>> get_jit_context("/alfred:1-plan", "/project")
31
- ['.moai/memory/spec-metadata.md']
31
+ ['.claude/skills/moai-alfred-spec-metadata-extended/reference.md']
32
32
  >>> get_jit_context("implement test", "/project")
33
33
  ['tests/']
34
34
  >>> get_jit_context("unknown", "/project")
@@ -49,8 +49,8 @@ def get_jit_context(prompt: str, cwd: str) -> list[str]:
49
49
 
50
50
  # Pattern matching
51
51
  patterns = {
52
- "/alfred:1-plan": [".moai/memory/spec-metadata.md"],
53
- "/alfred:2-run": [".moai/memory/development-guide.md"],
52
+ "/alfred:1-plan": [".claude/skills/moai-alfred-spec-metadata-extended/reference.md"],
53
+ "/alfred:2-run": [".claude/skills/moai-alfred-dev-guide/reference.md"],
54
54
  "test": ["tests/"],
55
55
  }
56
56
 
@@ -70,6 +70,7 @@ def find_project_root(start_path: str | Path = ".") -> Path:
70
70
 
71
71
  class TimeoutError(Exception):
72
72
  """Signal-based timeout exception"""
73
+
73
74
  pass
74
75
 
75
76
 
@@ -86,6 +87,7 @@ def timeout_handler(seconds: int):
86
87
  Raises:
87
88
  TimeoutError: If operation exceeds timeout
88
89
  """
90
+
89
91
  def _handle_timeout(signum, frame):
90
92
  raise TimeoutError(f"Operation timed out after {seconds} seconds")
91
93
 
@@ -427,19 +429,10 @@ def get_version_check_config(cwd: str) -> dict[str, Any]:
427
429
  - REFACTOR: Add validation and error handling
428
430
  """
429
431
  # TTL mapping by frequency
430
- TTL_BY_FREQUENCY = {
431
- "always": 0,
432
- "daily": 24,
433
- "weekly": 168,
434
- "never": float('inf')
435
- }
432
+ ttl_by_frequency = {"always": 0, "daily": 24, "weekly": 168, "never": float("inf")}
436
433
 
437
434
  # Default configuration
438
- defaults = {
439
- "enabled": True,
440
- "frequency": "daily",
441
- "cache_ttl_hours": 24
442
- }
435
+ defaults = {"enabled": True, "frequency": "daily", "cache_ttl_hours": 24}
443
436
 
444
437
  # Find project root to ensure we read config from correct location
445
438
  project_root = find_project_root(cwd)
@@ -461,21 +454,17 @@ def get_version_check_config(cwd: str) -> dict[str, Any]:
461
454
  frequency = moai_config.get("update_check_frequency", defaults["frequency"])
462
455
 
463
456
  # Validate frequency
464
- if frequency not in TTL_BY_FREQUENCY:
457
+ if frequency not in ttl_by_frequency:
465
458
  frequency = defaults["frequency"]
466
459
 
467
460
  # Calculate TTL from frequency
468
- cache_ttl_hours = TTL_BY_FREQUENCY[frequency]
461
+ cache_ttl_hours = ttl_by_frequency[frequency]
469
462
 
470
463
  # Allow explicit cache_ttl_hours override
471
464
  if "cache_ttl_hours" in version_check_config:
472
465
  cache_ttl_hours = version_check_config["cache_ttl_hours"]
473
466
 
474
- return {
475
- "enabled": enabled,
476
- "frequency": frequency,
477
- "cache_ttl_hours": cache_ttl_hours
478
- }
467
+ return {"enabled": enabled, "frequency": frequency, "cache_ttl_hours": cache_ttl_hours}
479
468
 
480
469
  except (OSError, json.JSONDecodeError, KeyError):
481
470
  # Config read or parse error - return defaults
@@ -570,36 +559,42 @@ def is_major_version_change(current: str, latest: str) -> bool:
570
559
  def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
571
560
  """Check MoAI-ADK current and latest version with caching and offline support.
572
561
 
562
+ ⭐ CRITICAL GUARANTEE: This function ALWAYS returns the current installed version.
563
+ Network failures, cache issues, and timeouts NEVER result in "unknown" version.
564
+
573
565
  Execution flow:
574
- 1. Try to load from cache (< 50ms)
575
- 2. If cache invalid, check network
576
- 3. If network available, query PyPI
577
- 4. If network unavailable, return current version only
578
- 5. Save result to cache for next time
566
+ 1. Get current installed version (ALWAYS succeeds) ← CRITICAL
567
+ 2. Build minimal result with current version
568
+ 3. Try to load from cache (< 50ms) - optional enhancement
569
+ 4. If cache valid, return cached latest info
570
+ 5. If cache invalid/miss, optionally query PyPI - optional enhancement
571
+ 6. Save result to cache for next time - optional
579
572
 
580
573
  Args:
581
574
  cwd: Project root directory (for cache location)
582
575
 
583
576
  Returns:
584
577
  dict with keys:
585
- - "current": Current installed version
586
- - "latest": Latest version available on PyPI
578
+ - "current": Current installed version (ALWAYS valid, never empty)
579
+ - "latest": Latest version available on PyPI (may be "unknown")
587
580
  - "update_available": Boolean indicating if update is available
588
581
  - "upgrade_command": Recommended upgrade command (if update available)
589
- - "release_notes_url": URL to release notes (Phase 3)
590
- - "is_major_update": Boolean indicating major version change (Phase 3)
582
+ - "release_notes_url": URL to release notes
583
+ - "is_major_update": Boolean indicating major version change
591
584
 
592
- Note:
593
- - Cache hit (< 24 hours): Returns in ~20ms, no network access
594
- - Cache miss + online: Query PyPI (1s timeout), cache result
595
- - Cache miss + offline: Return current version only (~100ms)
596
- - Offline + cached: Return from cache in ~20ms
585
+ Guarantees:
586
+ - Cache hit (< 24 hours): Returns in ~20ms, no network access βœ“
587
+ - Cache miss + online: Query PyPI (1s timeout), cache result βœ“
588
+ - Cache miss + offline: Return current version only (~100ms) βœ“
589
+ - Network timeout: Returns current + "unknown" latest (~50ms) βœ“
590
+ - Any exception: Always returns current version βœ“
597
591
 
598
592
  TDD History:
599
593
  - RED: 5 test scenarios (network detection, cache integration, offline mode)
600
594
  - GREEN: Integrate VersionCache with network detection
601
595
  - REFACTOR: Extract cache directory constant, improve error handling
602
596
  - Phase 3: Add release_notes_url and is_major_update fields (@CODE:VERSION-INTEGRATE-FIELDS-001)
597
+ - Phase 4: CRITICAL FIX - Always guarantee current version return (@CODE:VERSION-ALWAYS-VALID-001)
603
598
  """
604
599
  import importlib.util
605
600
  import urllib.error
@@ -613,13 +608,13 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
613
608
  if spec and spec.loader:
614
609
  version_cache_module = importlib.util.module_from_spec(spec)
615
610
  spec.loader.exec_module(version_cache_module)
616
- VersionCache = version_cache_module.VersionCache
611
+ version_cache_class = version_cache_module.VersionCache
617
612
  else:
618
613
  # Skip caching if module can't be loaded
619
- VersionCache = None
614
+ version_cache_class = None
620
615
  except (ImportError, OSError):
621
616
  # Graceful degradation: skip caching on import errors
622
- VersionCache = None
617
+ version_cache_class = None
623
618
 
624
619
  # 1. Find project root (ensure cache is always in correct location)
625
620
  # This prevents creating .moai/cache in wrong locations when hooks run
@@ -628,7 +623,7 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
628
623
 
629
624
  # 2. Initialize cache (skip if VersionCache couldn't be imported)
630
625
  cache_dir = project_root / CACHE_DIR_NAME
631
- version_cache = VersionCache(cache_dir) if VersionCache else None
626
+ version_cache = version_cache_class(cache_dir) if version_cache_class else None
632
627
 
633
628
  # 2. Get current installed version first (needed for cache validation)
634
629
  current_version = "unknown"
@@ -641,7 +636,7 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
641
636
  "current": "dev",
642
637
  "latest": "unknown",
643
638
  "update_available": False,
644
- "upgrade_command": ""
639
+ "upgrade_command": "",
645
640
  }
646
641
 
647
642
  # 3. Try to load from cache (fast path with version validation)
@@ -664,7 +659,7 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
664
659
  "current": current_version,
665
660
  "latest": "unknown",
666
661
  "update_available": False,
667
- "upgrade_command": ""
662
+ "upgrade_command": "",
668
663
  }
669
664
 
670
665
  # 5. Check if version check is enabled in config
@@ -695,7 +690,9 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
695
690
  release_url = project_urls.get("Changelog", "")
696
691
  if not release_url:
697
692
  # Fallback to GitHub releases URL pattern
698
- release_url = f"https://github.com/modu-ai/moai-adk/releases/tag/v{result['latest']}"
693
+ release_url = (
694
+ f"https://github.com/modu-ai/moai-adk/releases/tag/v{result['latest']}"
695
+ )
699
696
  result["release_notes_url"] = release_url
700
697
  except (KeyError, AttributeError, TypeError):
701
698
  result["release_notes_url"] = None
@@ -719,10 +716,12 @@ def get_package_version_info(cwd: str = ".") -> dict[str, Any]:
719
716
 
720
717
  if latest_parts > current_parts:
721
718
  result["update_available"] = True
722
- result["upgrade_command"] = f"uv pip install --upgrade moai-adk>={result['latest']}"
719
+ result["upgrade_command"] = "uv tool upgrade moai-adk"
723
720
 
724
721
  # Detect major version change
725
- result["is_major_update"] = is_major_version_change(result["current"], result["latest"])
722
+ result["is_major_update"] = is_major_version_change(
723
+ result["current"], result["latest"]
724
+ )
726
725
  else:
727
726
  result["is_major_update"] = False
728
727
  except (ValueError, AttributeError):
@@ -38,9 +38,22 @@ from pathlib import Path
38
38
  from typing import Iterable, List, Optional
39
39
 
40
40
  DEFAULT_CODE_EXTS = (
41
- ".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs",
42
- ".java", ".kt", ".rb", ".php", ".c", ".cpp", ".cs",
43
- ".swift", ".scala"
41
+ ".py",
42
+ ".ts",
43
+ ".tsx",
44
+ ".js",
45
+ ".jsx",
46
+ ".go",
47
+ ".rs",
48
+ ".java",
49
+ ".kt",
50
+ ".rb",
51
+ ".php",
52
+ ".c",
53
+ ".cpp",
54
+ ".cs",
55
+ ".swift",
56
+ ".scala",
44
57
  )
45
58
 
46
59
 
@@ -88,35 +101,49 @@ def _load_rules(cwd: str) -> List[Rule]:
88
101
 
89
102
  # Defaults (ordered)
90
103
  return [
91
- Rule(
92
- include=[".moai/specs/**", "**/SPEC-*/spec.md"],
93
- expect="@SPEC:",
94
- exclude=[]
95
- ),
104
+ Rule(include=[".moai/specs/**", "**/SPEC-*/spec.md"], expect="@SPEC:", exclude=[]),
96
105
  Rule(
97
106
  include=[
98
- "**/*_test.py", "**/test_*.py", "**/*.test.ts",
99
- "**/*.test.tsx", "**/*.test.js", "**/*.test.jsx",
100
- "**/*.test.go", "**/*.test.rs", "**/*.spec.ts",
101
- "**/*.spec.tsx", "tests/**"
107
+ "**/*_test.py",
108
+ "**/test_*.py",
109
+ "**/*.test.ts",
110
+ "**/*.test.tsx",
111
+ "**/*.test.js",
112
+ "**/*.test.jsx",
113
+ "**/*.test.go",
114
+ "**/*.test.rs",
115
+ "**/*.spec.ts",
116
+ "**/*.spec.tsx",
117
+ "tests/**",
102
118
  ],
103
119
  expect="@TEST:",
104
- exclude=[".claude/**"]
120
+ exclude=[".claude/**"],
105
121
  ),
106
122
  Rule(
107
123
  include=["docs/**/*.md", "**/README.md", "**/*.api.md"],
108
124
  expect="@DOC:",
109
- exclude=[".claude/**"]
125
+ exclude=[".claude/**"],
110
126
  ),
111
127
  Rule(
112
128
  include=["**/*"],
113
129
  expect="@CODE:",
114
130
  exclude=[
115
- "tests/**", "docs/**", ".moai/**", ".claude/**",
116
- "**/*.md", "**/*.json", "**/*.yml", "**/*.yaml",
117
- "**/*.toml", "**/*.lock", "**/*.svg", "**/*.png",
118
- "**/*.jpg", "**/*.jpeg", "**/*.gif"
119
- ]
131
+ "tests/**",
132
+ "docs/**",
133
+ ".moai/**",
134
+ ".claude/**",
135
+ "**/*.md",
136
+ "**/*.json",
137
+ "**/*.yml",
138
+ "**/*.yaml",
139
+ "**/*.toml",
140
+ "**/*.lock",
141
+ "**/*.svg",
142
+ "**/*.png",
143
+ "**/*.jpg",
144
+ "**/*.jpeg",
145
+ "**/*.gif",
146
+ ],
120
147
  ),
121
148
  ]
122
149
 
@@ -147,17 +174,22 @@ def _iter_recent_changes(cwd: str) -> Iterable[Path]:
147
174
  # Staged files
148
175
  r1 = subprocess.run(
149
176
  ["git", "diff", "--name-only", "--cached"],
150
- cwd=cwd, capture_output=True, text=True, timeout=1
177
+ cwd=cwd,
178
+ capture_output=True,
179
+ text=True,
180
+ timeout=1,
151
181
  )
152
182
  # Modified (unstaged) tracked files
153
183
  r2 = subprocess.run(
154
- ["git", "ls-files", "-m"],
155
- cwd=cwd, capture_output=True, text=True, timeout=1
184
+ ["git", "ls-files", "-m"], cwd=cwd, capture_output=True, text=True, timeout=1
156
185
  )
157
186
  # Untracked (other) files respecting .gitignore
158
187
  r3 = subprocess.run(
159
188
  ["git", "ls-files", "-o", "--exclude-standard"],
160
- cwd=cwd, capture_output=True, text=True, timeout=1
189
+ cwd=cwd,
190
+ capture_output=True,
191
+ text=True,
192
+ timeout=1,
161
193
  )
162
194
  names = set()
163
195
  if r1.returncode == 0:
@@ -86,7 +86,7 @@ class VersionCache:
86
86
  return False
87
87
 
88
88
  try:
89
- with open(self.cache_file, 'r') as f:
89
+ with open(self.cache_file, "r") as f:
90
90
  data = json.load(f)
91
91
 
92
92
  age_hours = self._calculate_age_hours(data["last_check"])
@@ -112,7 +112,7 @@ class VersionCache:
112
112
  return None
113
113
 
114
114
  try:
115
- with open(self.cache_file, 'r') as f:
115
+ with open(self.cache_file, "r") as f:
116
116
  return json.load(f)
117
117
  except (json.JSONDecodeError, OSError):
118
118
  # Graceful degradation on read errors
@@ -144,7 +144,7 @@ class VersionCache:
144
144
  version_info["last_check"] = datetime.now(timezone.utc).isoformat()
145
145
 
146
146
  # Write to cache file
147
- with open(self.cache_file, 'w') as f:
147
+ with open(self.cache_file, "w") as f:
148
148
  json.dump(version_info, f, indent=2)
149
149
 
150
150
  return True
@@ -186,7 +186,7 @@ class VersionCache:
186
186
  return 0.0
187
187
 
188
188
  try:
189
- with open(self.cache_file, 'r') as f:
189
+ with open(self.cache_file, "r") as f:
190
190
  data = json.load(f)
191
191
 
192
192
  return self._calculate_age_hours(data["last_check"])
@@ -4,21 +4,150 @@
4
4
  Notification, Stop, SubagentStop event handling
5
5
  """
6
6
 
7
+ import json
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
7
11
  from core import HookPayload, HookResult
8
12
 
9
13
 
14
+ def _get_command_state_file(cwd: str) -> Path:
15
+ """Get the path to command state tracking file"""
16
+ state_dir = Path(cwd) / ".moai" / "memory"
17
+ state_dir.mkdir(parents=True, exist_ok=True)
18
+ return state_dir / "command-execution-state.json"
19
+
20
+
21
+ def _load_command_state(cwd: str) -> dict:
22
+ """Load current command execution state"""
23
+ try:
24
+ state_file = _get_command_state_file(cwd)
25
+ if state_file.exists():
26
+ with open(state_file, "r", encoding="utf-8") as f:
27
+ return json.load(f)
28
+ except Exception:
29
+ pass
30
+ return {"last_command": None, "last_timestamp": None, "is_running": False}
31
+
32
+
33
+ def _save_command_state(cwd: str, state: dict) -> None:
34
+ """Save command execution state"""
35
+ try:
36
+ state_file = _get_command_state_file(cwd)
37
+ with open(state_file, "w", encoding="utf-8") as f:
38
+ json.dump(state, f, indent=2)
39
+ except Exception:
40
+ pass
41
+
42
+
43
+ def _is_duplicate_command(current_cmd: str, last_cmd: str, last_timestamp: str) -> bool:
44
+ """Check if current command is a duplicate of the last one within 3 seconds"""
45
+ if not last_cmd or not last_timestamp or current_cmd != last_cmd:
46
+ return False
47
+
48
+ try:
49
+ last_time = datetime.fromisoformat(last_timestamp)
50
+ current_time = datetime.now()
51
+ time_diff = (current_time - last_time).total_seconds()
52
+ # Consider it a duplicate if same command within 3 seconds
53
+ return time_diff < 3
54
+ except Exception:
55
+ return False
56
+
57
+
10
58
  def handle_notification(payload: HookPayload) -> HookResult:
11
- """Notification event handler (default implementation)"""
59
+ """Notification event handler
60
+
61
+ Detects and warns about duplicate command executions
62
+ (When the same /alfred: command is triggered multiple times within 3 seconds)
63
+ """
64
+ cwd = payload.get("cwd", ".")
65
+ notification = payload.get("notification", {})
66
+
67
+ # Extract command information from notification
68
+ current_cmd = None
69
+ if isinstance(notification, dict):
70
+ # Check if notification contains command information
71
+ text = notification.get("text", "") or str(notification)
72
+ if "/alfred:" in text:
73
+ # Extract /alfred: command
74
+ import re
75
+
76
+ match = re.search(r"/alfred:\S+", text)
77
+ if match:
78
+ current_cmd = match.group()
79
+
80
+ if not current_cmd:
81
+ return HookResult()
82
+
83
+ # Load current state
84
+ state = _load_command_state(cwd)
85
+ last_cmd = state.get("last_command")
86
+ last_timestamp = state.get("last_timestamp")
87
+
88
+ # Check for duplicate
89
+ if _is_duplicate_command(current_cmd, last_cmd, last_timestamp):
90
+ warning_msg = (
91
+ f"⚠️ Duplicate command detected: '{current_cmd}' "
92
+ f"is running multiple times within 3 seconds.\n"
93
+ f"This may indicate a system issue. Check logs in `.moai/logs/command-invocations.log`"
94
+ )
95
+
96
+ # Update state - mark as duplicate detected
97
+ state["duplicate_detected"] = True
98
+ state["duplicate_command"] = current_cmd
99
+ state["duplicate_timestamp"] = datetime.now().isoformat()
100
+ _save_command_state(cwd, state)
101
+
102
+ return HookResult(system_message=warning_msg, continue_execution=True)
103
+
104
+ # Update state with current command
105
+ state["last_command"] = current_cmd
106
+ state["last_timestamp"] = datetime.now().isoformat()
107
+ state["is_running"] = True
108
+ state["duplicate_detected"] = False
109
+ _save_command_state(cwd, state)
110
+
12
111
  return HookResult()
13
112
 
14
113
 
15
114
  def handle_stop(payload: HookPayload) -> HookResult:
16
- """Stop event handler (default implementation)"""
115
+ """Stop event handler
116
+
117
+ Marks command execution as complete
118
+ """
119
+ cwd = payload.get("cwd", ".")
120
+ state = _load_command_state(cwd)
121
+ state["is_running"] = False
122
+ state["last_timestamp"] = datetime.now().isoformat()
123
+ _save_command_state(cwd, state)
124
+
17
125
  return HookResult()
18
126
 
19
127
 
20
128
  def handle_subagent_stop(payload: HookPayload) -> HookResult:
21
- """SubagentStop event handler (default implementation)"""
129
+ """SubagentStop event handler
130
+
131
+ Records when a sub-agent finishes execution
132
+ """
133
+ cwd = payload.get("cwd", ".")
134
+
135
+ # Extract subagent name if available
136
+ subagent_name = (
137
+ payload.get("subagent", {}).get("name")
138
+ if isinstance(payload.get("subagent"), dict)
139
+ else None
140
+ )
141
+
142
+ try:
143
+ state_file = _get_command_state_file(cwd).parent / "subagent-execution.log"
144
+ timestamp = datetime.now().isoformat()
145
+
146
+ with open(state_file, "a", encoding="utf-8") as f:
147
+ f.write(f"{timestamp} | Subagent Stop | {subagent_name}\n")
148
+ except Exception:
149
+ pass
150
+
22
151
  return HookResult()
23
152
 
24
153
 
@@ -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"
@@ -4,6 +4,9 @@
4
4
  Handling the UserPromptSubmit event
5
5
  """
6
6
 
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
7
10
  from core import HookPayload, HookResult
8
11
  from core.context import get_jit_context
9
12
 
@@ -29,11 +32,27 @@ def handle_user_prompt_submit(payload: HookPayload) -> HookResult:
29
32
  - GREEN: Recommend documents by calling get_jit_context()
30
33
  - REFACTOR: Message conditional display (only when there is a file)
31
34
  - UPDATE: Migrated to Claude Code standard Hook schema with snake_case fields
35
+ - FEATURE: Command execution logging for tracking double-run debugging
32
36
  """
33
37
  user_prompt = payload.get("userPrompt", "")
34
38
  cwd = payload.get("cwd", ".")
35
39
  context_files = get_jit_context(user_prompt, cwd)
36
40
 
41
+ # Command execution logging (DEBUG feature for tracking invocations)
42
+ if user_prompt.startswith("/alfred:"):
43
+ try:
44
+ log_dir = Path(cwd) / ".moai" / "logs"
45
+ log_dir.mkdir(parents=True, exist_ok=True)
46
+
47
+ log_file = log_dir / "command-invocations.log"
48
+ timestamp = datetime.now().isoformat()
49
+
50
+ with open(log_file, "a", encoding="utf-8") as f:
51
+ f.write(f"{timestamp} | {user_prompt}\n")
52
+ except Exception:
53
+ # Silently fail if logging fails (don't interrupt main flow)
54
+ pass
55
+
37
56
  system_message = f"πŸ“Ž Loaded {len(context_files)} context file(s)" if context_files else None
38
57
 
39
58
  return HookResult(system_message=system_message, context_files=context_files)